diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 1ae0bec698d..6c90074094c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -63,4 +63,4 @@ export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", { web: "disk-local", }); -export const LOCAL_DATA = new StateDefinition("localData", "disk", { web: "disk-local" }); +export const CIPHERS_DISK = new StateDefinition("localData", "disk", { web: "disk-local" }); diff --git a/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.spec.ts index f30fc504366..b26ef38c330 100644 --- a/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.spec.ts @@ -72,7 +72,7 @@ describe("LocalDataMigrator", () => { let helper: MockProxy; let sut: LocalDataMigrator; const keyDefinitionLike = { - key: "local_data", + key: "ciphers_disk", stateDefinition: { name: "localData", }, diff --git a/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.ts b/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.ts index b41dc0617ca..0a25044ce6e 100644 --- a/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/23-move-local-data-to-state-provider.ts @@ -10,8 +10,8 @@ type LocalData = { lastLaunched?: number; }; -const LOCAL_DATA: KeyDefinitionLike = { - key: "local_data", +const CIPHERS_DISK: KeyDefinitionLike = { + key: "ciphers_disk", stateDefinition: { name: "localData", }, @@ -23,7 +23,7 @@ export class LocalDataMigrator extends Migrator<22, 23> { async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { const value = account?.localData; if (value != null) { - await helper.setToUser(userId, LOCAL_DATA, value); + await helper.setToUser(userId, CIPHERS_DISK, value); delete account.LocalData; await helper.set(userId, account); } @@ -35,14 +35,14 @@ export class LocalDataMigrator extends Migrator<22, 23> { async rollback(helper: MigrationHelper): Promise { const accounts = await helper.getAccounts(); async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { - const value = await helper.getFromUser(userId, LOCAL_DATA); + const value = await helper.getFromUser(userId, CIPHERS_DISK); if (account) { account.localData = Object.assign(account.localData ?? {}, { localData: value, }); await helper.set(userId, account); } - await helper.setToUser(userId, LOCAL_DATA, null); + await helper.setToUser(userId, CIPHERS_DISK, null); } await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index ecad46a4115..2c3a7852af0 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -88,4 +88,5 @@ export abstract class CipherService { asAdmin?: boolean, ) => Promise; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise; + decryptCiphers: (ciphers: Cipher[]) => Promise; } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 1452ffe7ee0..f8db7186d61 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherResponse } from "../response/cipher.response"; @@ -84,4 +86,8 @@ export class CipherData { this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); } } + + static fromJSON(obj: Jsonify) { + return Object.assign(new CipherData(), obj); + } } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 79cb91999a3..ce0fda56efa 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -20,7 +20,13 @@ 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, KeyDefinition, LOCAL_DATA, StateProvider } from "../../platform/state"; +import { + ActiveUserState, + KeyDefinition, + CIPHERS_DISK, + StateProvider, + DerivedState, +} from "../../platform/state"; import { UserKey, OrgKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; @@ -54,9 +60,11 @@ import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { PasswordHistoryView } from "../models/view/password-history.view"; +import { DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS } from "./key-state/ciphers.state"; + const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0"); -const LOCAL_DATA_KEY = new KeyDefinition>(LOCAL_DATA, "local_data", { +const CIPHERS_DISK_KEY = new KeyDefinition>(CIPHERS_DISK, "localData", { deserializer: (obj) => obj, }); @@ -66,10 +74,12 @@ export class CipherService implements CipherServiceAbstraction { ); localData$: Observable>; + ciphers$: Observable>; + cipherViews$: Observable; private localDataState: ActiveUserState>; - - private stateProviderFlag: boolean; + private encryptedCiphersState: ActiveUserState>; + private decryptedCiphersState: DerivedState; constructor( private cryptoService: CryptoService, @@ -84,21 +94,25 @@ export class CipherService implements CipherServiceAbstraction { private configService: ConfigServiceAbstraction, private stateProvider: StateProvider, ) { - this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); + this.localDataState = this.stateProvider.getActive(CIPHERS_DISK_KEY); + this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); + this.decryptedCiphersState = this.stateProvider.getDerived( + this.encryptedCiphersState.state$, + DECRYPTED_CIPHERS, + { cipherService: this }, + ); this.localData$ = this.localDataState.state$; - - //TODO remove this before opening the PR and references to 5273 - this.stateProviderFlag = false; + this.ciphers$ = this.encryptedCiphersState.state$; + this.cipherViews$ = this.decryptedCiphersState.state$; } async getDecryptedCipherCache(): Promise { - const decryptedCiphers = await this.stateService.getDecryptedCiphers(); + const decryptedCiphers = await firstValueFrom(this.cipherViews$); return decryptedCiphers; } async setDecryptedCipherCache(value: CipherView[]) { - await this.stateService.setDecryptedCiphers(value); if (this.searchService != null) { if (value == null) { this.searchService.clearIndex(); @@ -280,27 +294,20 @@ export class CipherService implements CipherServiceAbstraction { } async get(id: string): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id)) { return null; } - //5273 - let localData; - if (this.stateProviderFlag) { - localData = await firstValueFrom(this.localData$); - } else { - localData = await this.stateService.getLocalData(); - } + const localData = await firstValueFrom(this.localData$); return new Cipher(ciphers[id], localData ? localData[id] : null); } async getAll(): Promise { - //const localData = await firstValueFrom(this.localData$); - 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 @@ -318,7 +325,13 @@ export class CipherService implements CipherServiceAbstraction { return await this.getDecryptedCipherCache(); } - const ciphers = await this.getAll(); + const decCiphers = await this.decryptCiphers(await this.getAll()); + + await this.setDecryptedCipherCache(decCiphers); + return decCiphers; + } + + async decryptCiphers(ciphers: Cipher[]) { const orgKeys = await this.cryptoService.getOrgKeys(); const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); if (Object.keys(orgKeys).length === 0 && userKey == null) { @@ -346,7 +359,6 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - await this.setDecryptedCipherCache(decCiphers); return decCiphers; } @@ -465,13 +477,7 @@ export class CipherService implements CipherServiceAbstraction { } async updateLastUsedDate(id: string): Promise { - //5273 - let ciphersLocalData: { [cipherId: string]: LocalData } | Record; - if (this.stateProviderFlag) { - ciphersLocalData = await firstValueFrom(this.localData$); - } else { - ciphersLocalData = await this.stateService.getLocalData(); - } + let ciphersLocalData = await firstValueFrom(this.localData$); if (!ciphersLocalData) { ciphersLocalData = {}; @@ -485,14 +491,9 @@ export class CipherService implements CipherServiceAbstraction { }; } - //5273 - if (this.stateProviderFlag) { - await this.localDataState.update(() => ciphersLocalData); - } else { - await this.stateService.setLocalData(ciphersLocalData); - } + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await firstValueFrom(this.cipherViews$); if (!decryptedCipherCache) { return; } @@ -504,17 +505,12 @@ export class CipherService implements CipherServiceAbstraction { break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + //TODO Is this the right action? Or should the force be used only for clearing + await this.decryptedCiphersState.forceValue(decryptedCipherCache); } async updateLastLaunchedDate(id: string): Promise { - //5273 - let ciphersLocalData: { [cipherId: string]: LocalData } | Record; - if (this.stateProviderFlag) { - ciphersLocalData = await firstValueFrom(this.localData$); - } else { - ciphersLocalData = await this.stateService.getLocalData(); - } + let ciphersLocalData = await firstValueFrom(this.localData$); if (!ciphersLocalData) { ciphersLocalData = {}; @@ -528,14 +524,9 @@ export class CipherService implements CipherServiceAbstraction { }; } - //5273 - if (this.stateProviderFlag) { - await this.localDataState.update(() => ciphersLocalData); - } else { - await this.stateService.setLocalData(ciphersLocalData); - } + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await firstValueFrom(this.cipherViews$); if (!decryptedCipherCache) { return; } @@ -547,7 +538,8 @@ export class CipherService implements CipherServiceAbstraction { break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + //TODO Is this the right action? Or should the force be used only for clearing + await this.decryptedCiphersState.forceValue(decryptedCipherCache); } async saveNeverDomain(domain: string): Promise { @@ -730,7 +722,7 @@ export class CipherService implements CipherServiceAbstraction { } async upsert(cipher: CipherData | CipherData[]): Promise { - let ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { ciphers = {}; } @@ -749,7 +741,12 @@ export class CipherService implements CipherServiceAbstraction { async replace(ciphers: { [id: string]: CipherData }): Promise { await this.clearDecryptedCiphersState(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async clear(userId?: string): Promise { @@ -760,7 +757,7 @@ export class CipherService implements CipherServiceAbstraction { async moveManyWithServer(ids: string[], folderId: string): Promise { await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - let ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { ciphers = {}; } @@ -773,11 +770,16 @@ export class CipherService implements CipherServiceAbstraction { }); await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async delete(id: string | string[]): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } @@ -794,7 +796,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 deleteWithServer(id: string, asAdmin = false): Promise { @@ -818,7 +825,7 @@ export class CipherService implements CipherServiceAbstraction { } async deleteAttachment(id: string, attachmentId: string): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { @@ -832,7 +839,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 deleteAttachmentWithServer(id: string, attachmentId: string): Promise { @@ -915,7 +927,7 @@ export class CipherService implements CipherServiceAbstraction { } async softDelete(id: string | string[]): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } @@ -934,7 +946,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 softDeleteWithServer(id: string, asAdmin = false): Promise { @@ -961,7 +978,7 @@ 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; } @@ -981,7 +998,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 { @@ -1348,11 +1370,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.decryptedCiphersState.forceValue([]); this.clearSortedCiphers(); } diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts new file mode 100644 index 00000000000..bfb00d50a63 --- /dev/null +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -0,0 +1,23 @@ +import { Jsonify } from "type-fest"; + +import { CIPHERS_DISK, DeriveDefinition, KeyDefinition } from "../../../platform/state"; +import { CipherService } from "../../abstractions/cipher.service"; +import { CipherData } from "../../models/data/cipher.data"; +import { Cipher } from "../../models/domain/cipher"; +import { CipherView } from "../../models/view/cipher.view"; + +export const ENCRYPTED_CIPHERS = KeyDefinition.record(CIPHERS_DISK, "ciphers", { + deserializer: (obj: Jsonify) => CipherData.fromJSON(obj), +}); + +export const DECRYPTED_CIPHERS = DeriveDefinition.from< + Record, + CipherView[], + { cipherService: CipherService } +>(ENCRYPTED_CIPHERS, { + deserializer: (obj) => obj.map((c) => CipherView.fromJSON(c)), + derive: async (from, { cipherService }) => { + const ciphers = Object.values(from || {}).map((c) => new Cipher(c)); + return await cipherService.decryptCiphers(ciphers); + }, +});