mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
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:
parent
30f38dc916
commit
df9e6e21c9
@ -27,3 +27,6 @@ libs/.github
|
|||||||
|
|
||||||
# Github Workflows
|
# Github Workflows
|
||||||
.github/workflows
|
.github/workflows
|
||||||
|
|
||||||
|
# Forked library files
|
||||||
|
libs/common/src/types/deep-jsonify.ts
|
||||||
|
@ -19,6 +19,7 @@ describe("sessionSync decorator", () => {
|
|||||||
ctor: ctor,
|
ctor: ctor,
|
||||||
initializer: initializer,
|
initializer: initializer,
|
||||||
}),
|
}),
|
||||||
|
testClass.testProperty.complete(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import { BrowserApi } from "../../browser/browserApi";
|
|||||||
import { StateService } from "../../services/abstractions/state.service";
|
import { StateService } from "../../services/abstractions/state.service";
|
||||||
|
|
||||||
import { SessionSyncer } from "./session-syncer";
|
import { SessionSyncer } from "./session-syncer";
|
||||||
|
import { SyncedItemMetadata } from "./sync-item-metadata";
|
||||||
|
|
||||||
describe("session syncer", () => {
|
describe("session syncer", () => {
|
||||||
const propertyKey = "behaviorSubject";
|
const propertyKey = "behaviorSubject";
|
||||||
@ -140,12 +141,14 @@ describe("session syncer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update from message on emit from another instance", async () => {
|
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");
|
stateService.getFromSessionMemory.mockResolvedValue("test");
|
||||||
|
|
||||||
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" });
|
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" });
|
||||||
|
|
||||||
expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1);
|
expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1);
|
||||||
expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey);
|
expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder);
|
||||||
|
|
||||||
expect(nextSpy).toHaveBeenCalledTimes(1);
|
expect(nextSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(nextSpy).toHaveBeenCalledWith("test");
|
expect(nextSpy).toHaveBeenCalledWith("test");
|
||||||
|
@ -66,8 +66,8 @@ export class SessionSyncer {
|
|||||||
if (message.command != this.updateMessageCommand || message.id === this.id) {
|
if (message.command != this.updateMessageCommand || message.id === this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keyValuePair = await this.stateService.getFromSessionMemory(this.metaData.sessionKey);
|
const builder = SyncedItemMetadata.builder(this.metaData);
|
||||||
const value = SyncedItemMetadata.buildFromKeyValuePair(keyValuePair, this.metaData);
|
const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder);
|
||||||
this.ignoreNextUpdate = true;
|
this.ignoreNextUpdate = true;
|
||||||
this.behaviorSubject.next(value);
|
this.behaviorSubject.next(value);
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,15 @@ export class SyncedItemMetadata {
|
|||||||
initializer?: (keyValuePair: any) => any;
|
initializer?: (keyValuePair: any) => any;
|
||||||
initializeAsArray?: boolean;
|
initializeAsArray?: boolean;
|
||||||
|
|
||||||
static buildFromKeyValuePair(keyValuePair: any, metadata: SyncedItemMetadata): any {
|
static builder(metadata: SyncedItemMetadata): (o: any) => any {
|
||||||
const builder = SyncedItemMetadata.getBuilder(metadata);
|
const itemBuilder =
|
||||||
|
metadata.initializer != null
|
||||||
if (metadata.initializeAsArray) {
|
|
||||||
return keyValuePair.map((o: any) => builder(o));
|
|
||||||
} else {
|
|
||||||
return builder(keyValuePair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getBuilder(metadata: SyncedItemMetadata): (o: any) => any {
|
|
||||||
return metadata.initializer != null
|
|
||||||
? metadata.initializer
|
? metadata.initializer
|
||||||
: (o: any) => Object.assign(new metadata.ctor(), o);
|
: (o: any) => Object.assign(new metadata.ctor(), o);
|
||||||
|
if (metadata.initializeAsArray) {
|
||||||
|
return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o));
|
||||||
|
} else {
|
||||||
|
return (keyValuePair: any) => itemBuilder(keyValuePair);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,59 +1,39 @@
|
|||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
import { SyncedItemMetadata } from "./sync-item-metadata";
|
||||||
|
|
||||||
describe("build from key value pair", () => {
|
describe("builder", () => {
|
||||||
const propertyKey = "propertyKey";
|
const propertyKey = "propertyKey";
|
||||||
const key = "key";
|
const key = "key";
|
||||||
const initializer = (s: any) => "used initializer";
|
const initializer = (s: any) => "used initializer";
|
||||||
class TestClass {}
|
class TestClass {}
|
||||||
const ctor = TestClass;
|
const ctor = TestClass;
|
||||||
|
|
||||||
it("should call initializer if provided", () => {
|
it("should use initializer if provided", () => {
|
||||||
const actual = SyncedItemMetadata.buildFromKeyValuePair(
|
const metadata = { propertyKey, sessionKey: key, initializer };
|
||||||
{},
|
const builder = SyncedItemMetadata.builder(metadata);
|
||||||
{
|
expect(builder({})).toBe("used initializer");
|
||||||
propertyKey,
|
|
||||||
sessionKey: "key",
|
|
||||||
initializer: initializer,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(actual).toEqual("used initializer");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call ctor if provided", () => {
|
it("should use ctor if initializer is not provided", () => {
|
||||||
const expected = { provided: "value" };
|
const metadata = { propertyKey, sessionKey: key, ctor };
|
||||||
const actual = SyncedItemMetadata.buildFromKeyValuePair(expected, {
|
const builder = SyncedItemMetadata.builder(metadata);
|
||||||
propertyKey,
|
expect(builder({})).toBeInstanceOf(TestClass);
|
||||||
sessionKey: key,
|
|
||||||
ctor: ctor,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(actual).toBeInstanceOf(ctor);
|
it("should prefer initializer over ctor", () => {
|
||||||
expect(actual).toEqual(expect.objectContaining(expected));
|
const metadata = { propertyKey, sessionKey: key, ctor, initializer };
|
||||||
});
|
const builder = SyncedItemMetadata.builder(metadata);
|
||||||
|
expect(builder({})).toBe("used initializer");
|
||||||
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 honor initialize as array", () => {
|
it("should honor initialize as array", () => {
|
||||||
const actual = SyncedItemMetadata.buildFromKeyValuePair([1, 2], {
|
const metadata = {
|
||||||
propertyKey,
|
propertyKey,
|
||||||
sessionKey: key,
|
sessionKey: key,
|
||||||
initializer: initializer,
|
initializer: initializer,
|
||||||
initializeAsArray: true,
|
initializeAsArray: true,
|
||||||
});
|
};
|
||||||
|
const builder = SyncedItemMetadata.builder(metadata);
|
||||||
expect(actual).toEqual(["used initializer", "used initializer"]);
|
expect(builder([{}])).toBeInstanceOf(Array);
|
||||||
|
expect(builder([{}])[0]).toBe("used initializer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
||||||
|
|
||||||
@ -7,7 +9,7 @@ import { BrowserGroupingsComponentState } from "src/models/browserGroupingsCompo
|
|||||||
import { BrowserSendComponentState } from "src/models/browserSendComponentState";
|
import { BrowserSendComponentState } from "src/models/browserSendComponentState";
|
||||||
|
|
||||||
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
|
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>;
|
abstract setInSessionMemory(key: string, value: any): Promise<void>;
|
||||||
getBrowserGroupingComponentState: (
|
getBrowserGroupingComponentState: (
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
|
@ -96,6 +96,13 @@ describe("Browser Session Storage Service", () => {
|
|||||||
expect(cache.has("test")).toBe(true);
|
expect(cache.has("test")).toBe(true);
|
||||||
expect(cache.get("test")).toEqual(session.test);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
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 { EncString } from "@bitwarden/common/models/domain/encString";
|
||||||
|
import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
import { devFlag } from "../decorators/dev-flag.decorator";
|
import { devFlag } from "../decorators/dev-flag.decorator";
|
||||||
@ -15,7 +21,10 @@ const keys = {
|
|||||||
sessionKey: "session",
|
sessionKey: "session",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LocalBackedSessionStorageService extends AbstractCachedStorageService {
|
export class LocalBackedSessionStorageService
|
||||||
|
extends AbstractCachedStorageService
|
||||||
|
implements MemoryStorageServiceInterface
|
||||||
|
{
|
||||||
private cache = new Map<string, unknown>();
|
private cache = new Map<string, unknown>();
|
||||||
private localStorage = new BrowserLocalStorageService();
|
private localStorage = new BrowserLocalStorageService();
|
||||||
private sessionStorage = new BrowserMemoryStorageService();
|
private sessionStorage = new BrowserMemoryStorageService();
|
||||||
@ -27,21 +36,26 @@ export class LocalBackedSessionStorageService extends AbstractCachedStorageServi
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T> {
|
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
|
||||||
if (this.cache.has(key)) {
|
if (this.cache.has(key)) {
|
||||||
return this.cache.get(key) as T;
|
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());
|
const session = await this.getLocalSession(await this.getSessionEncKey());
|
||||||
if (session == null || !Object.keys(session).includes(key)) {
|
if (session == null || !Object.keys(session).includes(key)) {
|
||||||
return null;
|
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;
|
return this.cache.get(key) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
AbstractCachedStorageService,
|
MemoryStorageServiceInterface,
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
} from "@bitwarden/common/abstractions/storage.service";
|
} from "@bitwarden/common/abstractions/storage.service";
|
||||||
import { SendType } from "@bitwarden/common/enums/sendType";
|
import { SendType } from "@bitwarden/common/enums/sendType";
|
||||||
@ -49,7 +49,7 @@ describe("Browser State Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("direct memory storage access", () => {
|
describe("direct memory storage access", () => {
|
||||||
let memoryStorageService: AbstractCachedStorageService;
|
let memoryStorageService: LocalBackedSessionStorageService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass.
|
// We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass.
|
||||||
@ -79,12 +79,12 @@ describe("Browser State Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("state methods", () => {
|
describe("state methods", () => {
|
||||||
let memoryStorageService: SubstituteOf<AbstractStorageService>;
|
let memoryStorageService: SubstituteOf<AbstractStorageService & MemoryStorageServiceInterface>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
memoryStorageService = Substitute.for();
|
memoryStorageService = Substitute.for();
|
||||||
const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state)));
|
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(
|
sut = new StateService(
|
||||||
diskStorageService,
|
diskStorageService,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service";
|
import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||||
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
||||||
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
||||||
@ -17,9 +19,9 @@ export class StateService
|
|||||||
extends BaseStateService<GlobalState, Account>
|
extends BaseStateService<GlobalState, Account>
|
||||||
implements StateServiceAbstraction
|
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
|
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);
|
: await this.memoryStorageService.get<T>(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,4 +70,10 @@ describe("Utils Service", () => {
|
|||||||
expect(Utils.newGuid()).toMatch(validGuid);
|
expect(Utils.newGuid()).toMatch(validGuid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromByteStringToArray", () => {
|
||||||
|
it("should handle null", () => {
|
||||||
|
expect(Utils.fromByteStringToArray(null)).toEqual(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<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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -116,8 +116,8 @@ describe("State Migration Service", () => {
|
|||||||
key: "orgThreeEncKey",
|
key: "orgThreeEncKey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as any,
|
||||||
},
|
} as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5(
|
const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5(
|
||||||
|
@ -78,8 +78,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
||||||
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
|
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getDecodedToken: (options?: StorageOptions) => Promise<any>;
|
|
||||||
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
|
|
||||||
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
||||||
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
||||||
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
|
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { StorageOptions } from "../models/domain/storageOptions";
|
import { MemoryStorageOptions, StorageOptions } from "../models/domain/storageOptions";
|
||||||
|
|
||||||
export abstract class AbstractStorageService {
|
export abstract class AbstractStorageService {
|
||||||
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
|
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
|
||||||
@ -8,5 +8,9 @@ export abstract class AbstractStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractCachedStorageService extends 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>;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,9 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromByteStringToArray(str: string): Uint8Array {
|
static fromByteStringToArray(str: string): Uint8Array {
|
||||||
|
if (str == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const arr = new Uint8Array(str.length);
|
const arr = new Uint8Array(str.length);
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
arr[i] = str.charCodeAt(i);
|
arr[i] = str.charCodeAt(i);
|
||||||
|
55
libs/common/src/models/data/server-config.data.spec.ts
Normal file
55
libs/common/src/models/data/server-config.data.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ServerConfigResponse,
|
ServerConfigResponse,
|
||||||
ThirdPartyServerConfigResponse,
|
ThirdPartyServerConfigResponse,
|
||||||
@ -11,27 +13,38 @@ export class ServerConfigData {
|
|||||||
environment?: EnvironmentServerConfigData;
|
environment?: EnvironmentServerConfigData;
|
||||||
utcDate: string;
|
utcDate: string;
|
||||||
|
|
||||||
constructor(serverConfigReponse: ServerConfigResponse) {
|
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
|
||||||
this.version = serverConfigReponse?.version;
|
this.version = serverConfigResponse?.version;
|
||||||
this.gitHash = serverConfigReponse?.gitHash;
|
this.gitHash = serverConfigResponse?.gitHash;
|
||||||
this.server = serverConfigReponse?.server
|
this.server = serverConfigResponse?.server
|
||||||
? new ThirdPartyServerConfigData(serverConfigReponse.server)
|
? new ThirdPartyServerConfigData(serverConfigResponse.server)
|
||||||
: null;
|
: null;
|
||||||
this.utcDate = new Date().toISOString();
|
this.utcDate = new Date().toISOString();
|
||||||
this.environment = serverConfigReponse?.environment
|
this.environment = serverConfigResponse?.environment
|
||||||
? new EnvironmentServerConfigData(serverConfigReponse.environment)
|
? new EnvironmentServerConfigData(serverConfigResponse.environment)
|
||||||
: null;
|
: 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 {
|
export class ThirdPartyServerConfigData {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
constructor(response: ThirdPartyServerConfigResponse) {
|
constructor(response: Partial<ThirdPartyServerConfigResponse>) {
|
||||||
this.name = response.name;
|
this.name = response.name;
|
||||||
this.url = response.url;
|
this.url = response.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<ThirdPartyServerConfigData>): ThirdPartyServerConfigData {
|
||||||
|
return Object.assign(new ThirdPartyServerConfigData({}), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EnvironmentServerConfigData {
|
export class EnvironmentServerConfigData {
|
||||||
@ -41,11 +54,15 @@ export class EnvironmentServerConfigData {
|
|||||||
notifications: string;
|
notifications: string;
|
||||||
sso: string;
|
sso: string;
|
||||||
|
|
||||||
constructor(response: EnvironmentServerConfigResponse) {
|
constructor(response: Partial<EnvironmentServerConfigResponse>) {
|
||||||
this.vault = response.vault;
|
this.vault = response.vault;
|
||||||
this.api = response.api;
|
this.api = response.api;
|
||||||
this.identity = response.identity;
|
this.identity = response.identity;
|
||||||
this.notifications = response.notifications;
|
this.notifications = response.notifications;
|
||||||
this.sso = response.sso;
|
this.sso = response.sso;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<EnvironmentServerConfigData>): EnvironmentServerConfigData {
|
||||||
|
return Object.assign(new EnvironmentServerConfigData({}), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
62
libs/common/src/models/domain/account-keys.spec.ts
Normal file
62
libs/common/src/models/domain/account-keys.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
9
libs/common/src/models/domain/account-profile.spec.ts
Normal file
9
libs/common/src/models/domain/account-profile.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
libs/common/src/models/domain/account-settings.spec.ts
Normal file
24
libs/common/src/models/domain/account-settings.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
9
libs/common/src/models/domain/account-tokens.spec.ts
Normal file
9
libs/common/src/models/domain/account-tokens.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
libs/common/src/models/domain/account.spec.ts
Normal file
23
libs/common/src/models/domain/account.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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 { AuthenticationStatus } from "../../enums/authenticationStatus";
|
||||||
import { KdfType } from "../../enums/kdfType";
|
import { KdfType } from "../../enums/kdfType";
|
||||||
import { UriMatchType } from "../../enums/uriMatchType";
|
import { UriMatchType } from "../../enums/uriMatchType";
|
||||||
@ -24,7 +29,39 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
|||||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||||
encrypted?: TEncrypted;
|
encrypted?: TEncrypted;
|
||||||
decrypted?: TDecrypted;
|
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> {
|
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||||
@ -73,19 +110,66 @@ export class AccountKeys {
|
|||||||
>();
|
>();
|
||||||
organizationKeys?: EncryptionPair<
|
organizationKeys?: EncryptionPair<
|
||||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
{ [orgId: string]: EncryptedOrganizationKeyData },
|
||||||
Map<string, SymmetricCryptoKey>
|
Record<string, SymmetricCryptoKey>
|
||||||
> = new EncryptionPair<
|
> = new EncryptionPair<
|
||||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
{ [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,
|
any,
|
||||||
Map<string, SymmetricCryptoKey>
|
Record<string, SymmetricCryptoKey>
|
||||||
>();
|
>();
|
||||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||||
publicKey?: ArrayBuffer;
|
publicKey?: ArrayBuffer;
|
||||||
publicKeySerialized?: string;
|
|
||||||
apiKeyClientSecret?: 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 {
|
export class AccountProfile {
|
||||||
@ -106,6 +190,14 @@ export class AccountProfile {
|
|||||||
keyHash?: string;
|
keyHash?: string;
|
||||||
kdfIterations?: number;
|
kdfIterations?: number;
|
||||||
kdfType?: KdfType;
|
kdfType?: KdfType;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<AccountProfile>): AccountProfile {
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(new AccountProfile(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountSettings {
|
export class AccountSettings {
|
||||||
@ -142,6 +234,21 @@ export class AccountSettings {
|
|||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
serverConfig?: ServerConfigData;
|
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 = {
|
export type AccountSettingsSettings = {
|
||||||
@ -150,9 +257,16 @@ export type AccountSettingsSettings = {
|
|||||||
|
|
||||||
export class AccountTokens {
|
export class AccountTokens {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
decodedToken?: any;
|
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
securityStamp?: string;
|
securityStamp?: string;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<AccountTokens>): AccountTokens {
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(new AccountTokens(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Account {
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
34
libs/common/src/models/domain/encryption-pair.spec.ts
Normal file
34
libs/common/src/models/domain/encryption-pair.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
export class EnvironmentUrls {
|
export class EnvironmentUrls {
|
||||||
base: string = null;
|
base: string = null;
|
||||||
api: string = null;
|
api: string = null;
|
||||||
@ -7,4 +9,8 @@ export class EnvironmentUrls {
|
|||||||
events: string = null;
|
events: string = null;
|
||||||
webVault: string = null;
|
webVault: string = null;
|
||||||
keyConnector: string = null;
|
keyConnector: string = null;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<EnvironmentUrls>): EnvironmentUrls {
|
||||||
|
return Object.assign(new EnvironmentUrls(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
libs/common/src/models/domain/state.spec.ts
Normal file
28
libs/common/src/models/domain/state.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { Account } from "./account";
|
import { Account } from "./account";
|
||||||
import { GlobalState } from "./globalState";
|
import { GlobalState } from "./globalState";
|
||||||
|
|
||||||
@ -14,4 +16,30 @@ export class State<
|
|||||||
constructor(globals: TGlobalState) {
|
constructor(globals: TGlobalState) {
|
||||||
this.globals = globals;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { HtmlStorageLocation } from "../../enums/htmlStorageLocation";
|
import { HtmlStorageLocation } from "../../enums/htmlStorageLocation";
|
||||||
import { StorageLocation } from "../../enums/storageLocation";
|
import { StorageLocation } from "../../enums/storageLocation";
|
||||||
|
|
||||||
@ -8,3 +10,5 @@ export type StorageOptions = {
|
|||||||
htmlStorageLocation?: HtmlStorageLocation;
|
htmlStorageLocation?: HtmlStorageLocation;
|
||||||
keySuffix?: string;
|
keySuffix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MemoryStorageOptions<T> = StorageOptions & { deserializer?: (obj: Jsonify<T>) => T };
|
||||||
|
@ -393,7 +393,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
: firstValueFrom(this.settingsService.settings$).then(
|
: firstValueFrom(this.settingsService.settings$).then(
|
||||||
(settings: AccountSettingsSettings) => {
|
(settings: AccountSettingsSettings) => {
|
||||||
let matches: any[] = [];
|
let matches: any[] = [];
|
||||||
settings.equivalentDomains.forEach((eqDomain: any) => {
|
settings.equivalentDomains?.forEach((eqDomain: any) => {
|
||||||
if (eqDomain.length && eqDomain.indexOf(domain) >= 0) {
|
if (eqDomain.length && eqDomain.indexOf(domain) >= 0) {
|
||||||
matches = matches.concat(eqDomain);
|
matches = matches.concat(eqDomain);
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
async decryptToBytes(encThing: IEncrypted, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||||
|
@ -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>();
|
private store = new Map<string, any>();
|
||||||
|
|
||||||
get<T>(key: string): Promise<T> {
|
get<T>(key: string): Promise<T> {
|
||||||
|
@ -3,14 +3,16 @@ import { BehaviorSubject, concatMap } from "rxjs";
|
|||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
||||||
import { StateMigrationService } from "../abstractions/stateMigration.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 { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||||
import { KdfType } from "../enums/kdfType";
|
import { KdfType } from "../enums/kdfType";
|
||||||
import { StorageLocation } from "../enums/storageLocation";
|
import { StorageLocation } from "../enums/storageLocation";
|
||||||
import { ThemeType } from "../enums/themeType";
|
import { ThemeType } from "../enums/themeType";
|
||||||
import { UriMatchType } from "../enums/uriMatchType";
|
import { UriMatchType } from "../enums/uriMatchType";
|
||||||
import { StateFactory } from "../factories/stateFactory";
|
import { StateFactory } from "../factories/stateFactory";
|
||||||
import { Utils } from "../misc/utils";
|
|
||||||
import { CipherData } from "../models/data/cipherData";
|
import { CipherData } from "../models/data/cipherData";
|
||||||
import { CollectionData } from "../models/data/collectionData";
|
import { CollectionData } from "../models/data/collectionData";
|
||||||
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
|
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
|
||||||
@ -76,7 +78,7 @@ export class StateService<
|
|||||||
constructor(
|
constructor(
|
||||||
protected storageService: AbstractStorageService,
|
protected storageService: AbstractStorageService,
|
||||||
protected secureStorageService: AbstractStorageService,
|
protected secureStorageService: AbstractStorageService,
|
||||||
protected memoryStorageService: AbstractStorageService,
|
protected memoryStorageService: AbstractStorageService & MemoryStorageServiceInterface,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected stateMigrationService: StateMigrationService,
|
protected stateMigrationService: StateMigrationService,
|
||||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||||
@ -150,6 +152,9 @@ export class StateService<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.updateState(async (state) => {
|
await this.updateState(async (state) => {
|
||||||
|
if (state.accounts == null) {
|
||||||
|
state.accounts = {};
|
||||||
|
}
|
||||||
state.accounts[userId] = this.createAccount();
|
state.accounts[userId] = this.createAccount();
|
||||||
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
||||||
state.accounts[userId].profile = diskAccount.profile;
|
state.accounts[userId].profile = diskAccount.profile;
|
||||||
@ -494,11 +499,11 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
|
|
||||||
async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
|
async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
|
||||||
return (
|
const account = await this.getAccount(
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
)?.keys?.cryptoMasterKey;
|
);
|
||||||
|
return account?.keys?.cryptoMasterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
|
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
|
||||||
@ -604,23 +609,6 @@ export class StateService<
|
|||||||
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
|
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)
|
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
|
||||||
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
|
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
|
||||||
return (
|
return (
|
||||||
@ -657,11 +645,11 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
|
|
||||||
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
|
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
|
||||||
return (
|
const account = await this.getAccount(
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
)?.keys?.cryptoSymmetricKey?.decrypted;
|
);
|
||||||
|
return account?.keys?.cryptoSymmetricKey?.decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDecryptedCryptoSymmetricKey(
|
async setDecryptedCryptoSymmetricKey(
|
||||||
@ -678,14 +666,13 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
|
|
||||||
async getDecryptedOrganizationKeys(
|
async getDecryptedOrganizationKeys(
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<Map<string, SymmetricCryptoKey>> {
|
): Promise<Map<string, SymmetricCryptoKey>> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
return account?.keys?.organizationKeys?.decrypted;
|
return this.recordToMap(account?.keys?.organizationKeys?.decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDecryptedOrganizationKeys(
|
async setDecryptedOrganizationKeys(
|
||||||
@ -695,7 +682,7 @@ export class StateService<
|
|||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.keys.organizationKeys.decrypted = value;
|
account.keys.organizationKeys.decrypted = this.mapToRecord(value);
|
||||||
await this.saveAccount(
|
await this.saveAccount(
|
||||||
account,
|
account,
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
@ -725,7 +712,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototype(EncString)
|
|
||||||
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
|
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
@ -762,14 +748,9 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
|
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
|
||||||
const privateKey = (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
)?.keys?.privateKey;
|
)?.keys?.privateKey.decrypted;
|
||||||
let result = privateKey?.decrypted;
|
|
||||||
if (result == null && privateKey?.decryptedSerialized != null) {
|
|
||||||
result = Utils.fromByteStringToArray(privateKey.decryptedSerialized);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
||||||
@ -777,21 +758,19 @@ export class StateService<
|
|||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.keys.privateKey.decrypted = value;
|
account.keys.privateKey.decrypted = value;
|
||||||
account.keys.privateKey.decryptedSerialized =
|
|
||||||
value == null ? null : Utils.fromBufferToByteString(value);
|
|
||||||
await this.saveAccount(
|
await this.saveAccount(
|
||||||
account,
|
account,
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
|
|
||||||
async getDecryptedProviderKeys(
|
async getDecryptedProviderKeys(
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<Map<string, SymmetricCryptoKey>> {
|
): Promise<Map<string, SymmetricCryptoKey>> {
|
||||||
return (
|
const account = await this.getAccount(
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
)?.keys?.providerKeys?.decrypted;
|
);
|
||||||
|
return this.recordToMap(account?.keys?.providerKeys?.decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDecryptedProviderKeys(
|
async setDecryptedProviderKeys(
|
||||||
@ -801,7 +780,7 @@ export class StateService<
|
|||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.keys.providerKeys.decrypted = value;
|
account.keys.providerKeys.decrypted = this.mapToRecord(value);
|
||||||
await this.saveAccount(
|
await this.saveAccount(
|
||||||
account,
|
account,
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
@ -1538,7 +1517,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototype(EnvironmentUrls)
|
|
||||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
||||||
if ((await this.state())?.activeUserId == null) {
|
if ((await this.state())?.activeUserId == null) {
|
||||||
return await this.getGlobalEnvironmentUrls(options);
|
return await this.getGlobalEnvironmentUrls(options);
|
||||||
@ -2021,11 +1999,7 @@ export class StateService<
|
|||||||
const keys = (
|
const keys = (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
)?.keys;
|
)?.keys;
|
||||||
let result = keys?.publicKey;
|
return keys?.publicKey;
|
||||||
if (result == null && keys?.publicKeySerialized != null) {
|
|
||||||
result = Utils.fromByteStringToArray(keys.publicKeySerialized);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
|
||||||
@ -2033,7 +2007,6 @@ export class StateService<
|
|||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.keys.publicKey = value;
|
account.keys.publicKey = value;
|
||||||
account.keys.publicKeySerialized = value == null ? null : Utils.fromBufferToByteString(value);
|
|
||||||
await this.saveAccount(
|
await this.saveAccount(
|
||||||
account,
|
account,
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
@ -2741,8 +2714,11 @@ export class StateService<
|
|||||||
: await this.secureStorageService.save(`${options.userId}${key}`, value, options);
|
: await this.secureStorageService.save(`${options.userId}${key}`, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected state(): Promise<State<TGlobalState, TAccount>> {
|
protected async state(): Promise<State<TGlobalState, TAccount>> {
|
||||||
return this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state);
|
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> {
|
private async setState(state: State<TGlobalState, TAccount>): Promise<void> {
|
||||||
@ -2761,6 +2737,14 @@ export class StateService<
|
|||||||
await this.setState(updatedState);
|
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>(
|
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -12,7 +12,13 @@ import { OrganizationData } from "../models/data/organizationData";
|
|||||||
import { PolicyData } from "../models/data/policyData";
|
import { PolicyData } from "../models/data/policyData";
|
||||||
import { ProviderData } from "../models/data/providerData";
|
import { ProviderData } from "../models/data/providerData";
|
||||||
import { SendData } from "../models/data/sendData";
|
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 { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
||||||
import { GlobalState } from "../models/domain/globalState";
|
import { GlobalState } from "../models/domain/globalState";
|
||||||
@ -314,10 +320,10 @@ export class StateMigrationService<
|
|||||||
passwordGenerationOptions:
|
passwordGenerationOptions:
|
||||||
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
|
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
|
||||||
defaultAccount.settings.passwordGenerationOptions,
|
defaultAccount.settings.passwordGenerationOptions,
|
||||||
pinProtected: {
|
pinProtected: Object.assign(new EncryptionPair<string, EncString>(), {
|
||||||
decrypted: null,
|
decrypted: null,
|
||||||
encrypted: await this.get<string>(v1Keys.pinProtected),
|
encrypted: await this.get<string>(v1Keys.pinProtected),
|
||||||
},
|
}),
|
||||||
protectedPin: await this.get<string>(v1Keys.protectedPin),
|
protectedPin: await this.get<string>(v1Keys.protectedPin),
|
||||||
settings:
|
settings:
|
||||||
userId == null
|
userId == null
|
||||||
|
@ -93,11 +93,6 @@ export class TokenService implements TokenServiceAbstraction {
|
|||||||
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
|
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
|
||||||
|
|
||||||
async decodeToken(token?: string): Promise<any> {
|
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());
|
token = token ?? (await this.stateService.getAccessToken());
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
44
libs/common/src/types/deep-jsonify.ts
Normal file
44
libs/common/src/types/deep-jsonify.ts
Normal 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
|
Loading…
Reference in New Issue
Block a user