1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +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:
Matt Gibson
2022-09-22 07:51:14 -05:00
committed by GitHub
parent 30f38dc916
commit df9e6e21c9
37 changed files with 635 additions and 286 deletions

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) {