mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00: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:
@@ -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));
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user