1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[PM-5273] Migrate state in CipherService (#8314)

* PM-5273 Initial migration work for localData

* PM-5273 Encrypted and Decrypted ciphers migration to state provider

* pm-5273 Update references

* pm5273 Ensure prototype on cipher

* PM-5273 Add CipherId

* PM-5273 Remove migrated methods and updated references

* pm-5273 Fix versions

* PM-5273 Added missing options

* Conflict resolution

* Revert "Conflict resolution"

This reverts commit 0c0c2039ed.

* PM-5273 Fix PR comments

* Pm-5273 Fix comments

* PM-5273 Changed decryptedCiphers to use ActiveUserState

* PM-5273 Fix tests

* PM-5273 Fix pr comments
This commit is contained in:
Carlos Gonçalves
2024-04-16 17:37:03 +01:00
committed by GitHub
parent 62ed7e5abc
commit 06acdefa91
29 changed files with 525 additions and 305 deletions

View File

@@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { Observable, firstValueFrom } from "rxjs";
import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service";
@@ -21,13 +21,15 @@ import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserState, StateProvider } from "../../platform/state";
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
import { OrgKey, UserKey } from "../../types/key";
import { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { LocalData } from "../models/data/local.data";
import { Attachment } from "../models/domain/attachment";
import { Card } from "../models/domain/card";
import { Cipher } from "../models/domain/cipher";
@@ -54,6 +56,14 @@ import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view";
import { PasswordHistoryView } from "../models/view/password-history.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
import {
ENCRYPTED_CIPHERS,
LOCAL_DATA_KEY,
ADD_EDIT_CIPHER_INFO_KEY,
DECRYPTED_CIPHERS,
} from "./key-state/ciphers.state";
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
@@ -62,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction {
this.sortCiphersByLastUsed,
);
localData$: Observable<Record<CipherId, LocalData>>;
ciphers$: Observable<Record<CipherId, CipherData>>;
cipherViews$: Observable<Record<CipherId, CipherView>>;
addEditCipherInfo$: Observable<AddEditCipherInfo>;
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
constructor(
private cryptoService: CryptoService,
private domainSettingsService: DomainSettingsService,
@@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction {
private encryptService: EncryptService,
private cipherFileUploadService: CipherFileUploadService,
private configService: ConfigService,
) {}
private stateProvider: StateProvider,
) {
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
async getDecryptedCipherCache(): Promise<CipherView[]> {
const decryptedCiphers = await this.stateService.getDecryptedCiphers();
return decryptedCiphers;
this.localData$ = this.localDataState.state$;
this.ciphers$ = this.encryptedCiphersState.state$;
this.cipherViews$ = this.decryptedCiphersState.state$;
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
}
async setDecryptedCipherCache(value: CipherView[]) {
@@ -85,7 +111,7 @@ export class CipherService implements CipherServiceAbstraction {
// if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again.
// We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption.
if (value == null || value.length !== 0) {
await this.stateService.setDecryptedCiphers(value);
await this.setDecryptedCiphers(value);
}
if (this.searchService != null) {
if (value == null) {
@@ -96,6 +122,14 @@ export class CipherService implements CipherServiceAbstraction {
}
}
private async setDecryptedCiphers(value: CipherView[]) {
const cipherViews: { [id: string]: CipherView } = {};
value?.forEach((c) => {
cipherViews[c.id] = c;
});
await this.decryptedCiphersState.update(() => cipherViews);
}
async clearCache(userId?: string): Promise<void> {
await this.clearDecryptedCiphersState(userId);
}
@@ -268,24 +302,27 @@ export class CipherService implements CipherServiceAbstraction {
}
async get(id: string): Promise<Cipher> {
const ciphers = await this.stateService.getEncryptedCiphers();
const ciphers = await firstValueFrom(this.ciphers$);
// eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id)) {
return null;
}
const localData = await this.stateService.getLocalData();
return new Cipher(ciphers[id], localData ? localData[id] : null);
const localData = await firstValueFrom(this.localData$);
const cipherId = id as CipherId;
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
}
async getAll(): Promise<Cipher[]> {
const localData = await this.stateService.getLocalData();
const ciphers = await this.stateService.getEncryptedCiphers();
const localData = await firstValueFrom(this.localData$);
const ciphers = await firstValueFrom(this.ciphers$);
const response: Cipher[] = [];
for (const id in ciphers) {
// eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) {
response.push(new Cipher(ciphers[id], localData ? localData[id] : null));
const cipherId = id as CipherId;
response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null));
}
}
return response;
@@ -293,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction {
@sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise<CipherView[]> {
if ((await this.getDecryptedCipherCache()) != null) {
let decCiphers = await this.getDecryptedCiphers();
if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers();
return await this.getDecryptedCipherCache();
return await this.getDecryptedCiphers();
}
const ciphers = await this.getAll();
decCiphers = await this.decryptCiphers(await this.getAll());
await this.setDecryptedCipherCache(decCiphers);
return decCiphers;
}
private async getDecryptedCiphers() {
return Object.values(await firstValueFrom(this.cipherViews$));
}
private async decryptCiphers(ciphers: Cipher[]) {
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
if (Object.keys(orgKeys).length === 0 && userKey == null) {
@@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction {
.flat()
.sort(this.getLocaleSortingFunction());
await this.setDecryptedCipherCache(decCiphers);
return decCiphers;
}
@@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
if (reindexRequired) {
await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId);
}
}
@@ -448,22 +495,24 @@ export class CipherService implements CipherServiceAbstraction {
}
async updateLastUsedDate(id: string): Promise<void> {
let ciphersLocalData = await this.stateService.getLocalData();
let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) {
ciphersLocalData = {};
}
if (ciphersLocalData[id]) {
ciphersLocalData[id].lastUsedDate = new Date().getTime();
const cipherId = id as CipherId;
if (ciphersLocalData[cipherId]) {
ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
} else {
ciphersLocalData[id] = {
ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(),
};
}
await this.stateService.setLocalData(ciphersLocalData);
await this.localDataState.update(() => ciphersLocalData);
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
const decryptedCipherCache = await this.getDecryptedCiphers();
if (!decryptedCipherCache) {
return;
}
@@ -471,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i];
if (cached.id === id) {
cached.localData = ciphersLocalData[id];
cached.localData = ciphersLocalData[id as CipherId];
break;
}
}
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
await this.setDecryptedCiphers(decryptedCipherCache);
}
async updateLastLaunchedDate(id: string): Promise<void> {
let ciphersLocalData = await this.stateService.getLocalData();
let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) {
ciphersLocalData = {};
}
if (ciphersLocalData[id]) {
ciphersLocalData[id].lastLaunched = new Date().getTime();
const cipherId = id as CipherId;
if (ciphersLocalData[cipherId]) {
ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
} else {
ciphersLocalData[id] = {
ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(),
};
}
await this.stateService.setLocalData(ciphersLocalData);
await this.localDataState.update(() => ciphersLocalData);
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
const decryptedCipherCache = await this.getDecryptedCiphers();
if (!decryptedCipherCache) {
return;
}
@@ -502,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i];
if (cached.id === id) {
cached.localData = ciphersLocalData[id];
cached.localData = ciphersLocalData[id as CipherId];
break;
}
}
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
await this.setDecryptedCiphers(decryptedCipherCache);
}
async saveNeverDomain(domain: string): Promise<void> {
@@ -711,7 +762,7 @@ export class CipherService implements CipherServiceAbstraction {
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
// Update the local state
const ciphers = await this.stateService.getEncryptedCiphers();
const ciphers = await firstValueFrom(this.ciphers$);
for (const id of cipherIds) {
const cipher = ciphers[id];
@@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction {
}
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => ciphers);
}
async upsert(cipher: CipherData | CipherData[]): Promise<any> {
let ciphers = await this.stateService.getEncryptedCiphers();
if (ciphers == null) {
ciphers = {};
}
if (cipher instanceof CipherData) {
const c = cipher as CipherData;
ciphers[c.id] = c;
} else {
(cipher as CipherData[]).forEach((c) => {
ciphers[c.id] = c;
});
}
await this.replace(ciphers);
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
await this.updateEncryptedCipherState((current) => {
ciphers.forEach((c) => current[c.id as CipherId]);
return current;
});
}
async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
await this.updateEncryptedCipherState(() => ciphers);
}
private async updateEncryptedCipherState(
update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>,
) {
await this.clearDecryptedCiphersState();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update((current) => {
const result = update(current ?? {});
return result;
});
}
async clear(userId?: string): Promise<any> {
@@ -762,7 +812,7 @@ export class CipherService implements CipherServiceAbstraction {
async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
let ciphers = await this.stateService.getEncryptedCiphers();
let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
ciphers = {};
}
@@ -770,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction {
ids.forEach((id) => {
// eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) {
ciphers[id].folderId = folderId;
ciphers[id as CipherId].folderId = folderId;
}
});
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => ciphers);
}
async delete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers();
const ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
return;
}
if (typeof id === "string") {
if (ciphers[id] == null) {
const cipherId = id as CipherId;
if (ciphers[cipherId] == null) {
return;
}
delete ciphers[id];
delete ciphers[cipherId];
} else {
(id as string[]).forEach((i) => {
(id as CipherId[]).forEach((i) => {
delete ciphers[i];
});
}
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => ciphers);
}
async deleteWithServer(id: string, asAdmin = false): Promise<any> {
@@ -820,21 +871,26 @@ export class CipherService implements CipherServiceAbstraction {
}
async deleteAttachment(id: string, attachmentId: string): Promise<void> {
const ciphers = await this.stateService.getEncryptedCiphers();
let ciphers = await firstValueFrom(this.ciphers$);
const cipherId = id as CipherId;
// eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) {
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
return;
}
for (let i = 0; i < ciphers[id].attachments.length; i++) {
if (ciphers[id].attachments[i].id === attachmentId) {
ciphers[id].attachments.splice(i, 1);
for (let i = 0; i < ciphers[cipherId].attachments.length; i++) {
if (ciphers[cipherId].attachments[i].id === attachmentId) {
ciphers[cipherId].attachments.splice(i, 1);
}
}
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
}
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> {
@@ -917,12 +973,12 @@ export class CipherService implements CipherServiceAbstraction {
}
async softDelete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers();
let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
return;
}
const setDeletedDate = (cipherId: string) => {
const setDeletedDate = (cipherId: CipherId) => {
if (ciphers[cipherId] == null) {
return;
}
@@ -930,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction {
};
if (typeof id === "string") {
setDeletedDate(id);
setDeletedDate(id as CipherId);
} else {
(id as string[]).forEach(setDeletedDate);
}
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
}
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
@@ -963,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction {
async restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) {
const ciphers = await this.stateService.getEncryptedCiphers();
let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
return;
}
const clearDeletedDate = (c: { id: string; revisionDate: string }) => {
if (ciphers[c.id] == null) {
const cipherId = c.id as CipherId;
if (ciphers[cipherId] == null) {
return;
}
ciphers[c.id].deletedDate = null;
ciphers[c.id].revisionDate = c.revisionDate;
ciphers[cipherId].deletedDate = null;
ciphers[cipherId].revisionDate = c.revisionDate;
};
if (cipher.constructor.name === Array.name) {
@@ -983,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction {
}
await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers);
await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
}
async restoreWithServer(id: string, asAdmin = false): Promise<any> {
@@ -1025,6 +1092,10 @@ export class CipherService implements CipherServiceAbstraction {
);
}
async setAddEditCipherInfo(value: AddEditCipherInfo) {
await this.addEditCipherInfoState.update(() => value);
}
// Helpers
// In the case of a cipher that is being shared with an organization, we want to decrypt the
@@ -1350,11 +1421,11 @@ export class CipherService implements CipherServiceAbstraction {
}
private async clearEncryptedCiphersState(userId?: string) {
await this.stateService.setEncryptedCiphers(null, { userId: userId });
await this.encryptedCiphersState.update(() => ({}));
}
private async clearDecryptedCiphersState(userId?: string) {
await this.stateService.setDecryptedCiphers(null, { userId: userId });
await this.setDecryptedCiphers(null);
this.clearSortedCiphers();
}