[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 525 additions and 305 deletions

View File

@ -720,7 +720,7 @@ describe("NotificationBackground", () => {
); );
tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation(); tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation();
editItemSpy = jest.spyOn(notificationBackground as any, "editItem"); editItemSpy = jest.spyOn(notificationBackground as any, "editItem");
setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo"); setAddEditCipherInfoSpy = jest.spyOn(cipherService, "setAddEditCipherInfo");
openAddEditVaultItemPopoutSpy = jest.spyOn( openAddEditVaultItemPopoutSpy = jest.spyOn(
notificationBackground as any, notificationBackground as any,
"openAddEditVaultItemPopout", "openAddEditVaultItemPopout",

View File

@ -600,14 +600,14 @@ export default class NotificationBackground {
} }
/** /**
* Sets the add/edit cipher info in the state service * Sets the add/edit cipher info in the cipher service
* and opens the add/edit vault item popout. * and opens the add/edit vault item popout.
* *
* @param cipherView - The cipher to edit * @param cipherView - The cipher to edit
* @param senderTab - The tab that the message was sent from * @param senderTab - The tab that the message was sent from
*/ */
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
await this.stateService.setAddEditCipherInfo({ await this.cipherService.setAddEditCipherInfo({
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); });

View File

@ -592,7 +592,7 @@ describe("OverlayBackground", () => {
beforeEach(() => { beforeEach(() => {
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } }); sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
jest jest
.spyOn(overlayBackground["stateService"], "setAddEditCipherInfo") .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo")
.mockImplementation(); .mockImplementation();
jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation();
}); });
@ -600,7 +600,7 @@ describe("OverlayBackground", () => {
it("will not open the add edit popout window if the message does not have a login cipher provided", () => { it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender);
expect(overlayBackground["stateService"].setAddEditCipherInfo).not.toHaveBeenCalled(); expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled();
expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled();
}); });
@ -621,7 +621,7 @@ describe("OverlayBackground", () => {
); );
await flushPromises(); await flushPromises();
expect(overlayBackground["stateService"].setAddEditCipherInfo).toHaveBeenCalled(); expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled();
expect(BrowserApi.sendMessage).toHaveBeenCalledWith( expect(BrowserApi.sendMessage).toHaveBeenCalledWith(
"inlineAutofillMenuRefreshAddEditCipher", "inlineAutofillMenuRefreshAddEditCipher",
); );

View File

@ -636,7 +636,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
cipherView.type = CipherType.Login; cipherView.type = CipherType.Login;
cipherView.login = loginView; cipherView.login = loginView;
await this.stateService.setAddEditCipherInfo({ await this.cipherService.setAddEditCipherInfo({
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); });

View File

@ -663,12 +663,12 @@ export default class MainBackground {
this.encryptService, this.encryptService,
this.cipherFileUploadService, this.cipherFileUploadService,
this.configService, this.configService,
this.stateProvider,
); );
this.folderService = new FolderService( this.folderService = new FolderService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService, this.cipherService,
this.stateService,
this.stateProvider, this.stateProvider,
); );
this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.folderApiService = new FolderApiService(this.folderService, this.apiService);

View File

@ -7,6 +7,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
@ -42,6 +43,7 @@ export class AppComponent implements OnInit, OnDestroy {
private stateService: BrowserStateService, private stateService: BrowserStateService,
private browserSendStateService: BrowserSendStateService, private browserSendStateService: BrowserSendStateService,
private vaultBrowserStateService: VaultBrowserStateService, private vaultBrowserStateService: VaultBrowserStateService,
private cipherService: CipherService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private ngZone: NgZone,
private platformUtilsService: ForegroundPlatformUtilsService, private platformUtilsService: ForegroundPlatformUtilsService,
@ -161,7 +163,7 @@ export class AppComponent implements OnInit, OnDestroy {
await this.clearComponentStates(); await this.clearComponentStates();
} }
if (url.startsWith("/tabs/")) { if (url.startsWith("/tabs/")) {
await this.stateService.setAddEditCipherInfo(null); await this.cipherService.setAddEditCipherInfo(null);
} }
(window as any).previousPopupUrl = url; (window as any).previousPopupUrl = url;

View File

@ -1,6 +1,7 @@
import { Location } from "@angular/common"; import { Location } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -9,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
@ -19,6 +21,7 @@ import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher
export class GeneratorComponent extends BaseGeneratorComponent { export class GeneratorComponent extends BaseGeneratorComponent {
private addEditCipherInfo: AddEditCipherInfo; private addEditCipherInfo: AddEditCipherInfo;
private cipherState: CipherView; private cipherState: CipherView;
private cipherService: CipherService;
constructor( constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction, passwordGenerationService: PasswordGenerationServiceAbstraction,
@ -26,6 +29,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, i18nService: I18nService,
stateService: StateService, stateService: StateService,
cipherService: CipherService,
route: ActivatedRoute, route: ActivatedRoute,
logService: LogService, logService: LogService,
private location: Location, private location: Location,
@ -40,10 +44,11 @@ export class GeneratorComponent extends BaseGeneratorComponent {
route, route,
window, window,
); );
this.cipherService = cipherService;
} }
async ngOnInit() { async ngOnInit() {
this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$);
if (this.addEditCipherInfo != null) { if (this.addEditCipherInfo != null) {
this.cipherState = this.addEditCipherInfo.cipher; this.cipherState = this.addEditCipherInfo.cipher;
} }
@ -64,7 +69,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
this.addEditCipherInfo.cipher = this.cipherState; this.addEditCipherInfo.cipher = this.cipherState;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setAddEditCipherInfo(this.addEditCipherInfo); this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo);
this.close(); this.close();
} }

View File

@ -42,6 +42,7 @@ import {
i18nServiceFactory, i18nServiceFactory,
I18nServiceInitOptions, I18nServiceInitOptions,
} from "../../../platform/background/service-factories/i18n-service.factory"; } from "../../../platform/background/service-factories/i18n-service.factory";
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import { import {
stateServiceFactory, stateServiceFactory,
StateServiceInitOptions, StateServiceInitOptions,
@ -81,6 +82,7 @@ export function cipherServiceFactory(
await encryptServiceFactory(cache, opts), await encryptServiceFactory(cache, opts),
await cipherFileUploadServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts),
await configServiceFactory(cache, opts), await configServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
), ),
); );
} }

View File

@ -14,11 +14,10 @@ import {
i18nServiceFactory, i18nServiceFactory,
I18nServiceInitOptions, I18nServiceInitOptions,
} from "../../../platform/background/service-factories/i18n-service.factory"; } from "../../../platform/background/service-factories/i18n-service.factory";
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import { import {
stateServiceFactory as stateServiceFactory, stateProviderFactory,
StateServiceInitOptions, StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-service.factory"; } from "../../../platform/background/service-factories/state-provider.factory";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
@ -28,7 +27,7 @@ export type FolderServiceInitOptions = FolderServiceFactoryOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
CipherServiceInitOptions & CipherServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
StateServiceInitOptions; StateProviderInitOptions;
export function folderServiceFactory( export function folderServiceFactory(
cache: { folderService?: AbstractFolderService } & CachedServices, cache: { folderService?: AbstractFolderService } & CachedServices,
@ -43,7 +42,6 @@ export function folderServiceFactory(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts), await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts), await stateProviderFactory(cache, opts),
), ),
); );

View File

@ -304,7 +304,7 @@ export class AddEditComponent extends BaseAddEditComponent {
} }
private saveCipherState() { private saveCipherState() {
return this.stateService.setAddEditCipherInfo({ return this.cipherService.setAddEditCipherInfo({
cipher: this.cipher, cipher: this.cipher,
collectionIds: collectionIds:
this.collections == null this.collections == null

View File

@ -544,13 +544,13 @@ export class Main {
this.encryptService, this.encryptService,
this.cipherFileUploadService, this.cipherFileUploadService,
this.configService, this.configService,
this.stateProvider,
); );
this.folderService = new FolderService( this.folderService = new FolderService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService, this.cipherService,
this.stateService,
this.stateProvider, this.stateProvider,
); );

View File

@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { GeneratorComponent } from "./generator.component"; import { GeneratorComponent } from "./generator.component";
@ -54,6 +55,10 @@ describe("GeneratorComponent", () => {
provide: LogService, provide: LogService,
useValue: mock<LogService>(), useValue: mock<LogService>(),
}, },
{
provide: CipherService,
useValue: mock<CipherService>(),
},
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();

View File

@ -18,7 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Account } from "./account"; import { Account } from "./account";
import { GlobalState } from "./global-state"; import { GlobalState } from "./global-state";
@ -57,19 +56,6 @@ export class StateService extends BaseStateService<GlobalState, Account> {
await super.addAccount(account); await super.addAccount(account);
} }
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedCiphers(options);
}
async setEncryptedCiphers(
value: { [id: string]: CipherData },
options?: StorageOptions,
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedCiphers(value, options);
}
override async getLastSync(options?: StorageOptions): Promise<string> { override async getLastSync(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getLastSync(options); return await super.getLastSync(options);

View File

@ -411,6 +411,7 @@ const safeProviders: SafeProvider[] = [
encryptService: EncryptService, encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction, fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigService, configService: ConfigService,
stateProvider: StateProvider,
) => ) =>
new CipherService( new CipherService(
cryptoService, cryptoService,
@ -423,6 +424,7 @@ const safeProviders: SafeProvider[] = [
encryptService, encryptService,
fileUploadService, fileUploadService,
configService, configService,
stateProvider,
), ),
deps: [ deps: [
CryptoServiceAbstraction, CryptoServiceAbstraction,
@ -435,6 +437,7 @@ const safeProviders: SafeProvider[] = [
EncryptService, EncryptService,
CipherFileUploadServiceAbstraction, CipherFileUploadServiceAbstraction,
ConfigService, ConfigService,
StateProvider,
], ],
}), }),
safeProvider({ safeProvider({
@ -444,7 +447,6 @@ const safeProviders: SafeProvider[] = [
CryptoServiceAbstraction, CryptoServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
CipherServiceAbstraction, CipherServiceAbstraction,
StateServiceAbstraction,
StateProvider, StateProvider,
], ],
}), }),

View File

@ -1,6 +1,6 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { concatMap, Observable, Subject, takeUntil } from "rxjs"; import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@ -687,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
async loadAddEditCipherInfo(): Promise<boolean> { async loadAddEditCipherInfo(): Promise<boolean> {
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
const loadedSavedInfo = addEditCipherInfo != null; const loadedSavedInfo = addEditCipherInfo != null;
if (loadedSavedInfo) { if (loadedSavedInfo) {
@ -700,7 +700,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
} }
await this.stateService.setAddEditCipherInfo(null); await this.cipherService.setAddEditCipherInfo(null);
return loadedSavedInfo; return loadedSavedInfo;
} }

View File

@ -6,10 +6,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { KdfType } from "../enums"; import { KdfType } from "../enums";
import { Account } from "../models/domain/account"; import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
@ -38,8 +34,6 @@ export abstract class StateService<T extends Account = Account> {
clean: (options?: StorageOptions) => Promise<UserId>; clean: (options?: StorageOptions) => Promise<UserId>;
init: (initOptions?: InitOptions) => Promise<void>; init: (initOptions?: InitOptions) => Promise<void>;
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
/** /**
* Gets the user's auto key * Gets the user's auto key
*/ */
@ -104,8 +98,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use setUserKeyBiometric instead * @deprecated For migration purposes only, use setUserKeyBiometric instead
*/ */
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>; setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedPasswordGenerationHistory: ( getDecryptedPasswordGenerationHistory: (
options?: StorageOptions, options?: StorageOptions,
) => Promise<GeneratedPasswordHistory[]>; ) => Promise<GeneratedPasswordHistory[]>;
@ -134,11 +126,6 @@ export abstract class StateService<T extends Account = Account> {
value: boolean, value: boolean,
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => Promise<void>;
getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>;
setEncryptedCiphers: (
value: { [id: string]: CipherData },
options?: StorageOptions,
) => Promise<void>;
getEncryptedPasswordGenerationHistory: ( getEncryptedPasswordGenerationHistory: (
options?: StorageOptions, options?: StorageOptions,
) => Promise<GeneratedPasswordHistory[]>; ) => Promise<GeneratedPasswordHistory[]>;
@ -165,11 +152,6 @@ export abstract class StateService<T extends Account = Account> {
setLastActive: (value: number, options?: StorageOptions) => Promise<void>; setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
getLastSync: (options?: StorageOptions) => Promise<string>; getLastSync: (options?: StorageOptions) => Promise<string>;
setLastSync: (value: string, options?: StorageOptions) => Promise<void>; setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
getLocalData: (options?: StorageOptions) => Promise<{ [cipherId: string]: LocalData }>;
setLocalData: (
value: { [cipherId: string]: LocalData },
options?: StorageOptions,
) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;

View File

@ -8,9 +8,6 @@ import {
} from "../../../tools/generator/password"; } from "../../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { CipherData } from "../../../vault/models/data/cipher.data";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
import { KdfType } from "../../enums"; import { KdfType } from "../../enums";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
@ -61,28 +58,17 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
} }
export class AccountData { export class AccountData {
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<
CipherData,
CipherView
>();
localData?: any;
passwordGenerationHistory?: EncryptionPair< passwordGenerationHistory?: EncryptionPair<
GeneratedPasswordHistory[], GeneratedPasswordHistory[],
GeneratedPasswordHistory[] GeneratedPasswordHistory[]
> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>(); > = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
addEditCipherInfo?: AddEditCipherInfo;
static fromJSON(obj: DeepJsonify<AccountData>): AccountData { static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
if (obj == null) { if (obj == null) {
return null; return null;
} }
return Object.assign(new AccountData(), obj, { return Object.assign(new AccountData(), obj);
addEditCipherInfo: {
cipher: CipherView.fromJSON(obj?.addEditCipherInfo?.cipher),
collectionIds: obj?.addEditCipherInfo?.collectionIds,
},
});
} }
} }

View File

@ -9,10 +9,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { EnvironmentService } from "../abstractions/environment.service"; import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { import {
@ -221,34 +217,6 @@ export class StateService<
return currentUser as UserId; return currentUser as UserId;
} }
async getAddEditCipherInfo(options?: StorageOptions): Promise<AddEditCipherInfo> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
// ensure prototype on cipher
const raw = account?.data?.addEditCipherInfo;
return raw == null
? null
: {
cipher:
raw?.cipher.toJSON != null
? raw.cipher
: CipherView.fromJSON(raw?.cipher as Jsonify<CipherView>),
collectionIds: raw?.collectionIds,
};
}
async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.data.addEditCipherInfo = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
/** /**
* user key when using the "never" option of vault timeout * user key when using the "never" option of vault timeout
*/ */
@ -465,24 +433,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
} }
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.data?.ciphers?.decrypted;
}
async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.data.ciphers.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
@withPrototypeForArrayMembers(GeneratedPasswordHistory) @withPrototypeForArrayMembers(GeneratedPasswordHistory)
async getDecryptedPasswordGenerationHistory( async getDecryptedPasswordGenerationHistory(
options?: StorageOptions, options?: StorageOptions,
@ -621,27 +571,6 @@ export class StateService<
); );
} }
@withPrototypeForObjectValues(CipherData)
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
)?.data?.ciphers?.encrypted;
}
async setEncryptedCiphers(
value: { [id: string]: CipherData },
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
account.data.ciphers.encrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
}
/** /**
* @deprecated Use UserKey instead * @deprecated Use UserKey instead
*/ */
@ -805,26 +734,6 @@ export class StateService<
); );
} }
async getLocalData(options?: StorageOptions): Promise<{ [cipherId: string]: LocalData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.data?.localData;
}
async setLocalData(
value: { [cipherId: string]: LocalData },
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
account.data.localData = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
}
async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise<boolean> { async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@ -1510,50 +1419,3 @@ function withPrototypeForArrayMembers<T>(
}; };
}; };
} }
function withPrototypeForObjectValues<T>(
valuesConstructor: new (...args: any[]) => T,
valuesConverter: (input: any) => T = (i) => i,
): (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) => { value: (...args: any[]) => Promise<{ [key: string]: T }> } {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
return {
value: function (...args: any[]) {
const originalResult: Promise<{ [key: string]: T }> = originalMethod.apply(this, args);
if (!Utils.isPromise(originalResult)) {
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 {
for (const [key, val] of Object.entries(result)) {
result[key] =
val == null || val.constructor.name === valuesConstructor.prototype.constructor.name
? valuesConverter(val)
: valuesConverter(
Object.create(
valuesConstructor.prototype,
Object.getOwnPropertyDescriptors(val),
),
);
}
return result as { [key: string]: T };
}
});
},
};
};
}

View File

@ -135,3 +135,8 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk",
}); });
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory"); export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory");
export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" });
export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
web: "disk-local",
});
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory");

View File

@ -53,6 +53,7 @@ import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-m
import { SendMigrator } from "./migrations/54-move-encrypted-sends"; import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@ -60,8 +61,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 56; export const CURRENT_VERSION = 57;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -119,7 +119,8 @@ export function createMigrationBuilder() {
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
.with(SendMigrator, 53, 54) .with(SendMigrator, 53, 54)
.with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(MoveMasterKeyStateToProviderMigrator, 54, 55)
.with(AuthRequestMigrator, 55, CURRENT_VERSION); .with(AuthRequestMigrator, 55, 56)
.with(CipherServiceMigrator, 56, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@ -0,0 +1,170 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
CIPHERS_DISK,
CIPHERS_DISK_LOCAL,
CipherServiceMigrator,
} from "./57-move-cipher-service-to-state-provider";
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2"],
user1: {
data: {
localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
},
},
user2: {
data: {
otherStuff: "otherStuff5",
},
},
};
}
function rollbackJSON() {
return {
user_user1_ciphersLocal_localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
user_user1_ciphers_ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2"],
user1: {
data: {},
},
user2: {
data: {
localData: {
otherStuff: "otherStuff3",
},
ciphers: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
},
};
}
describe("CipherServiceMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: CipherServiceMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 56);
sut = new CipherServiceMigrator(56, 57);
});
it("should remove local data and ciphers from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("user1", {
data: {},
});
});
it("should migrate localData and ciphers to state provider for accounts that have the data", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK_LOCAL, {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
});
expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK, {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
});
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK_LOCAL, any());
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK, any());
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 57);
sut = new CipherServiceMigrator(56, 57);
});
it.each(["user1", "user2"])("should null out new values", async (userId) => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK_LOCAL, null);
expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK, null);
});
it("should add back localData and ciphers to all accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("user1", {
data: {
localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
},
});
});
it("should not add data back if data wasn't migrated or acct doesn't exist", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
});
});
});

View File

@ -0,0 +1,79 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedAccountType = {
data: {
localData?: unknown;
ciphers?: unknown;
};
};
export const CIPHERS_DISK_LOCAL: KeyDefinitionLike = {
key: "localData",
stateDefinition: {
name: "ciphersLocal",
},
};
export const CIPHERS_DISK: KeyDefinitionLike = {
key: "ciphers",
stateDefinition: {
name: "ciphers",
},
};
export class CipherServiceMigrator extends Migrator<56, 57> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
let updatedAccount = false;
//Migrate localData
const localData = account?.data?.localData;
if (localData != null) {
await helper.setToUser(userId, CIPHERS_DISK_LOCAL, localData);
delete account.data.localData;
updatedAccount = true;
}
//Migrate ciphers
const ciphers = account?.data?.ciphers;
if (ciphers != null) {
await helper.setToUser(userId, CIPHERS_DISK, ciphers);
delete account.data.ciphers;
updatedAccount = true;
}
if (updatedAccount) {
await helper.set(userId, account);
}
}
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
//rollback localData
const localData = await helper.getFromUser(userId, CIPHERS_DISK_LOCAL);
if (account.data && localData != null) {
account.data.localData = localData;
await helper.set(userId, account);
}
await helper.setToUser(userId, CIPHERS_DISK_LOCAL, null);
//rollback ciphers
const ciphers = await helper.getFromUser(userId, CIPHERS_DISK);
if (account.data && ciphers != null) {
account.data.ciphers = ciphers;
await helper.set(userId, account);
}
await helper.setToUser(userId, CIPHERS_DISK, null);
}
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
}
}

View File

@ -1,3 +1,5 @@
import { Observable } from "rxjs";
import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
@ -7,8 +9,13 @@ import { Cipher } from "../models/domain/cipher";
import { Field } from "../models/domain/field"; import { Field } from "../models/domain/field";
import { CipherView } from "../models/view/cipher.view"; import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view"; import { FieldView } from "../models/view/field.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
export abstract class CipherService { export abstract class CipherService {
/**
* An observable monitoring the add/edit cipher info saved to memory.
*/
addEditCipherInfo$: Observable<AddEditCipherInfo>;
clearCache: (userId?: string) => Promise<void>; clearCache: (userId?: string) => Promise<void>;
encrypt: ( encrypt: (
model: CipherView, model: CipherView,
@ -102,4 +109,5 @@ export abstract class CipherService {
asAdmin?: boolean, asAdmin?: boolean,
) => Promise<void>; ) => Promise<void>;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
} }

View File

@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type"; import { CipherType } from "../../enums/cipher-type";
import { CipherResponse } from "../response/cipher.response"; import { CipherResponse } from "../response/cipher.response";
@ -84,4 +86,8 @@ export class CipherData {
this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph));
} }
} }
static fromJSON(obj: Jsonify<CipherData>) {
return Object.assign(new CipherData(), obj);
}
} }

View File

@ -1,6 +1,8 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray } from "../../../spec/utils"; import { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service"; import { SearchService } from "../../abstractions/search.service";
@ -12,10 +14,12 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service"; import { ContainerService } from "../../platform/services/container.service";
import { UserId } from "../../types/guid";
import { CipherKey, OrgKey } from "../../types/key"; import { CipherKey, OrgKey } from "../../types/key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums"; import { FieldType } from "../enums";
@ -97,6 +101,8 @@ const cipherData: CipherData = {
}, },
], ],
}; };
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
describe("Cipher Service", () => { describe("Cipher Service", () => {
const cryptoService = mock<CryptoService>(); const cryptoService = mock<CryptoService>();
@ -109,6 +115,8 @@ describe("Cipher Service", () => {
const searchService = mock<SearchService>(); const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const configService = mock<ConfigService>(); const configService = mock<ConfigService>();
accountService = mockAccountServiceWith(mockUserId);
const stateProvider = new FakeStateProvider(accountService);
let cipherService: CipherService; let cipherService: CipherService;
let cipherObj: Cipher; let cipherObj: Cipher;
@ -130,6 +138,7 @@ describe("Cipher Service", () => {
encryptService, encryptService,
cipherFileUploadService, cipherFileUploadService,
configService, configService,
stateProvider,
); );
cipherObj = new Cipher(cipherData); cipherObj = new Cipher(cipherData);

View File

@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs"; import { Observable, firstValueFrom } from "rxjs";
import { SemVer } from "semver"; import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service"; 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 { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserState, StateProvider } from "../../platform/state";
import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; 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 { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums"; import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data"; import { CipherData } from "../models/data/cipher.data";
import { LocalData } from "../models/data/local.data";
import { Attachment } from "../models/domain/attachment"; import { Attachment } from "../models/domain/attachment";
import { Card } from "../models/domain/card"; import { Card } from "../models/domain/card";
import { Cipher } from "../models/domain/cipher"; 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 { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view"; import { FieldView } from "../models/view/field.view";
import { PasswordHistoryView } from "../models/view/password-history.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"); const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
@ -62,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction {
this.sortCiphersByLastUsed, 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( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction {
private encryptService: EncryptService, private encryptService: EncryptService,
private cipherFileUploadService: CipherFileUploadService, private cipherFileUploadService: CipherFileUploadService,
private configService: ConfigService, 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[]> { this.localData$ = this.localDataState.state$;
const decryptedCiphers = await this.stateService.getDecryptedCiphers(); this.ciphers$ = this.encryptedCiphersState.state$;
return decryptedCiphers; this.cipherViews$ = this.decryptedCiphersState.state$;
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
} }
async setDecryptedCipherCache(value: CipherView[]) { 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. // 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. // 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) { if (value == null || value.length !== 0) {
await this.stateService.setDecryptedCiphers(value); await this.setDecryptedCiphers(value);
} }
if (this.searchService != null) { if (this.searchService != null) {
if (value == 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> { async clearCache(userId?: string): Promise<void> {
await this.clearDecryptedCiphersState(userId); await this.clearDecryptedCiphersState(userId);
} }
@ -268,24 +302,27 @@ export class CipherService implements CipherServiceAbstraction {
} }
async get(id: string): Promise<Cipher> { async get(id: string): Promise<Cipher> {
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await firstValueFrom(this.ciphers$);
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id)) { if (ciphers == null || !ciphers.hasOwnProperty(id)) {
return null; return null;
} }
const localData = await this.stateService.getLocalData(); const localData = await firstValueFrom(this.localData$);
return new Cipher(ciphers[id], localData ? localData[id] : null); const cipherId = id as CipherId;
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
} }
async getAll(): Promise<Cipher[]> { async getAll(): Promise<Cipher[]> {
const localData = await this.stateService.getLocalData(); const localData = await firstValueFrom(this.localData$);
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await firstValueFrom(this.ciphers$);
const response: Cipher[] = []; const response: Cipher[] = [];
for (const id in ciphers) { for (const id in ciphers) {
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) { 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; return response;
@ -293,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction {
@sequentialize(() => "getAllDecrypted") @sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise<CipherView[]> { async getAllDecrypted(): Promise<CipherView[]> {
if ((await this.getDecryptedCipherCache()) != null) { let decCiphers = await this.getDecryptedCiphers();
if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers(); 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 orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
if (Object.keys(orgKeys).length === 0 && userKey == null) { if (Object.keys(orgKeys).length === 0 && userKey == null) {
@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction {
.flat() .flat()
.sort(this.getLocaleSortingFunction()); .sort(this.getLocaleSortingFunction());
await this.setDecryptedCipherCache(decCiphers);
return decCiphers; return decCiphers;
} }
@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
this.searchService != null && this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
if (reindexRequired) { 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> { async updateLastUsedDate(id: string): Promise<void> {
let ciphersLocalData = await this.stateService.getLocalData(); let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) { if (!ciphersLocalData) {
ciphersLocalData = {}; ciphersLocalData = {};
} }
if (ciphersLocalData[id]) { const cipherId = id as CipherId;
ciphersLocalData[id].lastUsedDate = new Date().getTime(); if (ciphersLocalData[cipherId]) {
ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
} else { } else {
ciphersLocalData[id] = { ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(), 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) { if (!decryptedCipherCache) {
return; return;
} }
@ -471,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) { for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i]; const cached = decryptedCipherCache[i];
if (cached.id === id) { if (cached.id === id) {
cached.localData = ciphersLocalData[id]; cached.localData = ciphersLocalData[id as CipherId];
break; break;
} }
} }
await this.stateService.setDecryptedCiphers(decryptedCipherCache); await this.setDecryptedCiphers(decryptedCipherCache);
} }
async updateLastLaunchedDate(id: string): Promise<void> { async updateLastLaunchedDate(id: string): Promise<void> {
let ciphersLocalData = await this.stateService.getLocalData(); let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) { if (!ciphersLocalData) {
ciphersLocalData = {}; ciphersLocalData = {};
} }
if (ciphersLocalData[id]) { const cipherId = id as CipherId;
ciphersLocalData[id].lastLaunched = new Date().getTime(); if (ciphersLocalData[cipherId]) {
ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
} else { } else {
ciphersLocalData[id] = { ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(), 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) { if (!decryptedCipherCache) {
return; return;
} }
@ -502,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) { for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i]; const cached = decryptedCipherCache[i];
if (cached.id === id) { if (cached.id === id) {
cached.localData = ciphersLocalData[id]; cached.localData = ciphersLocalData[id as CipherId];
break; break;
} }
} }
await this.stateService.setDecryptedCiphers(decryptedCipherCache); await this.setDecryptedCiphers(decryptedCipherCache);
} }
async saveNeverDomain(domain: string): Promise<void> { 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); await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
// Update the local state // Update the local state
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await firstValueFrom(this.ciphers$);
for (const id of cipherIds) { for (const id of cipherIds) {
const cipher = ciphers[id]; const cipher = ciphers[id];
@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => ciphers);
} }
async upsert(cipher: CipherData | CipherData[]): Promise<any> { async upsert(cipher: CipherData | CipherData[]): Promise<any> {
let ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
if (ciphers == null) { await this.updateEncryptedCipherState((current) => {
ciphers = {}; ciphers.forEach((c) => current[c.id as CipherId]);
} return current;
});
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);
} }
async replace(ciphers: { [id: string]: CipherData }): Promise<any> { 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.clearDecryptedCiphersState();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update((current) => {
const result = update(current ?? {});
return result;
});
} }
async clear(userId?: string): Promise<any> { async clear(userId?: string): Promise<any> {
@ -762,7 +812,7 @@ export class CipherService implements CipherServiceAbstraction {
async moveManyWithServer(ids: string[], folderId: string): Promise<any> { async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
let ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@ -770,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction {
ids.forEach((id) => { ids.forEach((id) => {
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) { if (ciphers.hasOwnProperty(id)) {
ciphers[id].folderId = folderId; ciphers[id as CipherId].folderId = folderId;
} }
}); });
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => ciphers);
} }
async delete(id: string | string[]): Promise<any> { async delete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
if (typeof id === "string") { if (typeof id === "string") {
if (ciphers[id] == null) { const cipherId = id as CipherId;
if (ciphers[cipherId] == null) {
return; return;
} }
delete ciphers[id]; delete ciphers[cipherId];
} else { } else {
(id as string[]).forEach((i) => { (id as CipherId[]).forEach((i) => {
delete ciphers[i]; delete ciphers[i];
}); });
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => ciphers);
} }
async deleteWithServer(id: string, asAdmin = false): Promise<any> { 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> { 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 // 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; return;
} }
for (let i = 0; i < ciphers[id].attachments.length; i++) { for (let i = 0; i < ciphers[cipherId].attachments.length; i++) {
if (ciphers[id].attachments[i].id === attachmentId) { if (ciphers[cipherId].attachments[i].id === attachmentId) {
ciphers[id].attachments.splice(i, 1); ciphers[cipherId].attachments.splice(i, 1);
} }
} }
await this.clearCache(); 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> { async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> {
@ -917,12 +973,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
async softDelete(id: string | string[]): Promise<any> { async softDelete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
const setDeletedDate = (cipherId: string) => { const setDeletedDate = (cipherId: CipherId) => {
if (ciphers[cipherId] == null) { if (ciphers[cipherId] == null) {
return; return;
} }
@ -930,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction {
}; };
if (typeof id === "string") { if (typeof id === "string") {
setDeletedDate(id); setDeletedDate(id as CipherId);
} else { } else {
(id as string[]).forEach(setDeletedDate); (id as string[]).forEach(setDeletedDate);
} }
await this.clearCache(); 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> { async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
@ -963,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction {
async restore( async restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) { ) {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
const clearDeletedDate = (c: { id: string; revisionDate: string }) => { const clearDeletedDate = (c: { id: string; revisionDate: string }) => {
if (ciphers[c.id] == null) { const cipherId = c.id as CipherId;
if (ciphers[cipherId] == null) {
return; return;
} }
ciphers[c.id].deletedDate = null; ciphers[cipherId].deletedDate = null;
ciphers[c.id].revisionDate = c.revisionDate; ciphers[cipherId].revisionDate = c.revisionDate;
}; };
if (cipher.constructor.name === Array.name) { if (cipher.constructor.name === Array.name) {
@ -983,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); 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> { 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 // Helpers
// In the case of a cipher that is being shared with an organization, we want to decrypt the // 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) { private async clearEncryptedCiphersState(userId?: string) {
await this.stateService.setEncryptedCiphers(null, { userId: userId }); await this.encryptedCiphersState.update(() => ({}));
} }
private async clearDecryptedCiphersState(userId?: string) { private async clearDecryptedCiphersState(userId?: string) {
await this.stateService.setDecryptedCiphers(null, { userId: userId }); await this.setDecryptedCiphers(null);
this.clearSortedCiphers(); this.clearSortedCiphers();
} }

View File

@ -8,7 +8,6 @@ import { FakeStateProvider } from "../../../../spec/fake-state-provider";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@ -27,7 +26,6 @@ describe("Folder Service", () => {
let encryptService: MockProxy<EncryptService>; let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>; let cipherService: MockProxy<CipherService>;
let stateService: MockProxy<StateService>;
let stateProvider: FakeStateProvider; let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
@ -39,7 +37,6 @@ describe("Folder Service", () => {
encryptService = mock<EncryptService>(); encryptService = mock<EncryptService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
cipherService = mock<CipherService>(); cipherService = mock<CipherService>();
stateService = mock<StateService>();
accountService = mockAccountServiceWith(mockUserId); accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
@ -52,13 +49,7 @@ describe("Folder Service", () => {
); );
encryptService.decryptToUtf8.mockResolvedValue("DEC"); encryptService.decryptToUtf8.mockResolvedValue("DEC");
folderService = new FolderService( folderService = new FolderService(cryptoService, i18nService, cipherService, stateProvider);
cryptoService,
i18nService,
cipherService,
stateService,
stateProvider,
);
folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS); folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS);

View File

@ -2,17 +2,16 @@ import { Observable, firstValueFrom, map } from "rxjs";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state"; import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service"; import { CipherService } from "../../../vault/abstractions/cipher.service";
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { CipherData } from "../../../vault/models/data/cipher.data";
import { FolderData } from "../../../vault/models/data/folder.data"; import { FolderData } from "../../../vault/models/data/folder.data";
import { Folder } from "../../../vault/models/domain/folder"; import { Folder } from "../../../vault/models/domain/folder";
import { FolderView } from "../../../vault/models/view/folder.view"; import { FolderView } from "../../../vault/models/view/folder.view";
import { Cipher } from "../../models/domain/cipher";
import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state";
export class FolderService implements InternalFolderServiceAbstraction { export class FolderService implements InternalFolderServiceAbstraction {
@ -26,7 +25,6 @@ export class FolderService implements InternalFolderServiceAbstraction {
private cryptoService: CryptoService, private cryptoService: CryptoService,
private i18nService: I18nService, private i18nService: I18nService,
private cipherService: CipherService, private cipherService: CipherService,
private stateService: StateService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
) { ) {
this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS); this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS);
@ -144,9 +142,9 @@ export class FolderService implements InternalFolderServiceAbstraction {
}); });
// Items in a deleted folder are re-assigned to "No Folder" // Items in a deleted folder are re-assigned to "No Folder"
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await this.cipherService.getAll();
if (ciphers != null) { if (ciphers != null) {
const updates: CipherData[] = []; const updates: Cipher[] = [];
for (const cId in ciphers) { for (const cId in ciphers) {
if (ciphers[cId].folderId === id) { if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null; ciphers[cId].folderId = null;
@ -156,7 +154,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
if (updates.length > 0) { if (updates.length > 0) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.cipherService.upsert(updates); this.cipherService.upsert(updates.map((c) => c.toCipherData()));
} }
} }
} }

View File

@ -0,0 +1,52 @@
import { Jsonify } from "type-fest";
import {
CIPHERS_DISK,
CIPHERS_DISK_LOCAL,
CIPHERS_MEMORY,
KeyDefinition,
} from "../../../platform/state";
import { CipherId } from "../../../types/guid";
import { CipherData } from "../../models/data/cipher.data";
import { LocalData } from "../../models/data/local.data";
import { CipherView } from "../../models/view/cipher.view";
import { AddEditCipherInfo } from "../../types/add-edit-cipher-info";
export const ENCRYPTED_CIPHERS = KeyDefinition.record<CipherData>(CIPHERS_DISK, "ciphers", {
deserializer: (obj: Jsonify<CipherData>) => CipherData.fromJSON(obj),
});
export const DECRYPTED_CIPHERS = KeyDefinition.record<CipherView>(
CIPHERS_MEMORY,
"decryptedCiphers",
{
deserializer: (cipher: Jsonify<CipherView>) => CipherView.fromJSON(cipher),
},
);
export const LOCAL_DATA_KEY = new KeyDefinition<Record<CipherId, LocalData>>(
CIPHERS_DISK_LOCAL,
"localData",
{
deserializer: (localData) => localData,
},
);
export const ADD_EDIT_CIPHER_INFO_KEY = new KeyDefinition<AddEditCipherInfo>(
CIPHERS_MEMORY,
"addEditCipherInfo",
{
deserializer: (addEditCipherInfo: AddEditCipherInfo) => {
if (addEditCipherInfo == null) {
return null;
}
const cipher =
addEditCipherInfo?.cipher.toJSON != null
? addEditCipherInfo.cipher
: CipherView.fromJSON(addEditCipherInfo?.cipher as Jsonify<CipherView>);
return { cipher, collectionIds: addEditCipherInfo.collectionIds };
},
},
);