From 06acdefa914e3c6c84403aea2fd815e2e51e7c48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?=
Date: Tue, 16 Apr 2024 17:37:03 +0100
Subject: [PATCH 01/86] [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 0c0c2039edd4ee442456b6c5a4863a92b9a50e16.
* 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
---
.../notification.background.spec.ts | 2 +-
.../background/notification.background.ts | 4 +-
.../background/overlay.background.spec.ts | 6 +-
.../autofill/background/overlay.background.ts | 2 +-
.../browser/src/background/main.background.ts | 2 +-
apps/browser/src/popup/app.component.ts | 4 +-
.../popup/generator/generator.component.ts | 9 +-
.../cipher-service.factory.ts | 2 +
.../folder-service.factory.ts | 10 +-
.../components/vault/add-edit.component.ts | 2 +-
apps/cli/src/bw.ts | 2 +-
.../src/app/tools/generator.component.spec.ts | 5 +
apps/web/src/app/core/state/state.service.ts | 14 --
.../src/services/jslib-services.module.ts | 4 +-
.../vault/components/add-edit.component.ts | 6 +-
.../platform/abstractions/state.service.ts | 18 --
.../src/platform/models/domain/account.ts | 16 +-
.../src/platform/services/state.service.ts | 138 -----------
.../src/platform/state/state-definitions.ts | 5 +
libs/common/src/state-migrations/migrate.ts | 7 +-
...e-cipher-service-to-state-provider.spec.ts | 170 +++++++++++++
...7-move-cipher-service-to-state-provider.ts | 79 ++++++
.../src/vault/abstractions/cipher.service.ts | 8 +
.../src/vault/models/data/cipher.data.ts | 6 +
.../src/vault/services/cipher.service.spec.ts | 9 +
.../src/vault/services/cipher.service.ts | 227 ++++++++++++------
.../services/folder/folder.service.spec.ts | 11 +-
.../vault/services/folder/folder.service.ts | 10 +-
.../vault/services/key-state/ciphers.state.ts | 52 ++++
29 files changed, 525 insertions(+), 305 deletions(-)
create mode 100644 libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts
create mode 100644 libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts
create mode 100644 libs/common/src/vault/services/key-state/ciphers.state.ts
diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts
index 93750ece07..45f095aee9 100644
--- a/apps/browser/src/autofill/background/notification.background.spec.ts
+++ b/apps/browser/src/autofill/background/notification.background.spec.ts
@@ -720,7 +720,7 @@ describe("NotificationBackground", () => {
);
tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation();
editItemSpy = jest.spyOn(notificationBackground as any, "editItem");
- setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo");
+ setAddEditCipherInfoSpy = jest.spyOn(cipherService, "setAddEditCipherInfo");
openAddEditVaultItemPopoutSpy = jest.spyOn(
notificationBackground as any,
"openAddEditVaultItemPopout",
diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts
index 74e6147505..9b65e4db0b 100644
--- a/apps/browser/src/autofill/background/notification.background.ts
+++ b/apps/browser/src/autofill/background/notification.background.ts
@@ -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.
*
* @param cipherView - The cipher to edit
* @param senderTab - The tab that the message was sent from
*/
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
- await this.stateService.setAddEditCipherInfo({
+ await this.cipherService.setAddEditCipherInfo({
cipher: cipherView,
collectionIds: cipherView.collectionIds,
});
diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts
index 2599c1825e..e65397a62b 100644
--- a/apps/browser/src/autofill/background/overlay.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay.background.spec.ts
@@ -592,7 +592,7 @@ describe("OverlayBackground", () => {
beforeEach(() => {
sender = mock({ tab: { id: 1 } });
jest
- .spyOn(overlayBackground["stateService"], "setAddEditCipherInfo")
+ .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo")
.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", () => {
sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender);
- expect(overlayBackground["stateService"].setAddEditCipherInfo).not.toHaveBeenCalled();
+ expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled();
expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled();
});
@@ -621,7 +621,7 @@ describe("OverlayBackground", () => {
);
await flushPromises();
- expect(overlayBackground["stateService"].setAddEditCipherInfo).toHaveBeenCalled();
+ expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled();
expect(BrowserApi.sendMessage).toHaveBeenCalledWith(
"inlineAutofillMenuRefreshAddEditCipher",
);
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index 50fb80ef1b..551263525e 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -636,7 +636,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
cipherView.type = CipherType.Login;
cipherView.login = loginView;
- await this.stateService.setAddEditCipherInfo({
+ await this.cipherService.setAddEditCipherInfo({
cipher: cipherView,
collectionIds: cipherView.collectionIds,
});
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 105e7e2a38..642510b4de 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -663,12 +663,12 @@ export default class MainBackground {
this.encryptService,
this.cipherFileUploadService,
this.configService,
+ this.stateProvider,
);
this.folderService = new FolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
- this.stateService,
this.stateProvider,
);
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts
index b0fdaec4fc..c224e652f6 100644
--- a/apps/browser/src/popup/app.component.ts
+++ b/apps/browser/src/popup/app.component.ts
@@ -7,6 +7,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { BrowserApi } from "../platform/browser/browser-api";
@@ -42,6 +43,7 @@ export class AppComponent implements OnInit, OnDestroy {
private stateService: BrowserStateService,
private browserSendStateService: BrowserSendStateService,
private vaultBrowserStateService: VaultBrowserStateService,
+ private cipherService: CipherService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
private platformUtilsService: ForegroundPlatformUtilsService,
@@ -161,7 +163,7 @@ export class AppComponent implements OnInit, OnDestroy {
await this.clearComponentStates();
}
if (url.startsWith("/tabs/")) {
- await this.stateService.setAddEditCipherInfo(null);
+ await this.cipherService.setAddEditCipherInfo(null);
}
(window as any).previousPopupUrl = url;
diff --git a/apps/browser/src/tools/popup/generator/generator.component.ts b/apps/browser/src/tools/popup/generator/generator.component.ts
index c683b6f4a6..0c11c28f27 100644
--- a/apps/browser/src/tools/popup/generator/generator.component.ts
+++ b/apps/browser/src/tools/popup/generator/generator.component.ts
@@ -1,6 +1,7 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
+import { firstValueFrom } from "rxjs";
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
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 { 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 {
private addEditCipherInfo: AddEditCipherInfo;
private cipherState: CipherView;
+ private cipherService: CipherService;
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
@@ -26,6 +29,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
stateService: StateService,
+ cipherService: CipherService,
route: ActivatedRoute,
logService: LogService,
private location: Location,
@@ -40,10 +44,11 @@ export class GeneratorComponent extends BaseGeneratorComponent {
route,
window,
);
+ this.cipherService = cipherService;
}
async ngOnInit() {
- this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
+ this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$);
if (this.addEditCipherInfo != null) {
this.cipherState = this.addEditCipherInfo.cipher;
}
@@ -64,7 +69,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
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.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stateService.setAddEditCipherInfo(this.addEditCipherInfo);
+ this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo);
this.close();
}
diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts
index 8ffeca72bc..57366ea8c0 100644
--- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts
+++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts
@@ -42,6 +42,7 @@ import {
i18nServiceFactory,
I18nServiceInitOptions,
} from "../../../platform/background/service-factories/i18n-service.factory";
+import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
@@ -81,6 +82,7 @@ export function cipherServiceFactory(
await encryptServiceFactory(cache, opts),
await cipherFileUploadServiceFactory(cache, opts),
await configServiceFactory(cache, opts),
+ await stateProviderFactory(cache, opts),
),
);
}
diff --git a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts
index 72847a0536..0593dc904c 100644
--- a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts
+++ b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts
@@ -14,11 +14,10 @@ import {
i18nServiceFactory,
I18nServiceInitOptions,
} from "../../../platform/background/service-factories/i18n-service.factory";
-import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import {
- stateServiceFactory as stateServiceFactory,
- StateServiceInitOptions,
-} from "../../../platform/background/service-factories/state-service.factory";
+ stateProviderFactory,
+ StateProviderInitOptions,
+} from "../../../platform/background/service-factories/state-provider.factory";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
@@ -28,7 +27,7 @@ export type FolderServiceInitOptions = FolderServiceFactoryOptions &
CryptoServiceInitOptions &
CipherServiceInitOptions &
I18nServiceInitOptions &
- StateServiceInitOptions;
+ StateProviderInitOptions;
export function folderServiceFactory(
cache: { folderService?: AbstractFolderService } & CachedServices,
@@ -43,7 +42,6 @@ export function folderServiceFactory(
await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts),
- await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
);
diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
index b27a986231..a566b054c0 100644
--- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
+++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
@@ -304,7 +304,7 @@ export class AddEditComponent extends BaseAddEditComponent {
}
private saveCipherState() {
- return this.stateService.setAddEditCipherInfo({
+ return this.cipherService.setAddEditCipherInfo({
cipher: this.cipher,
collectionIds:
this.collections == null
diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts
index 4228eba965..f02d7da49c 100644
--- a/apps/cli/src/bw.ts
+++ b/apps/cli/src/bw.ts
@@ -544,13 +544,13 @@ export class Main {
this.encryptService,
this.cipherFileUploadService,
this.configService,
+ this.stateProvider,
);
this.folderService = new FolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
- this.stateService,
this.stateProvider,
);
diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts
index 53f919a596..51b5bf93a2 100644
--- a/apps/desktop/src/app/tools/generator.component.spec.ts
+++ b/apps/desktop/src/app/tools/generator.component.spec.ts
@@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { GeneratorComponent } from "./generator.component";
@@ -54,6 +55,10 @@ describe("GeneratorComponent", () => {
provide: LogService,
useValue: mock(),
},
+ {
+ provide: CipherService,
+ useValue: mock(),
+ },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts
index 54e456d34c..1ae62d8591 100644
--- a/apps/web/src/app/core/state/state.service.ts
+++ b/apps/web/src/app/core/state/state.service.ts
@@ -18,7 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
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 { GlobalState } from "./global-state";
@@ -57,19 +56,6 @@ export class StateService extends BaseStateService {
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 {
- options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
- return await super.setEncryptedCiphers(value, options);
- }
-
override async getLastSync(options?: StorageOptions): Promise {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getLastSync(options);
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index b311000fb8..dbb94f6753 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -411,6 +411,7 @@ const safeProviders: SafeProvider[] = [
encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigService,
+ stateProvider: StateProvider,
) =>
new CipherService(
cryptoService,
@@ -423,6 +424,7 @@ const safeProviders: SafeProvider[] = [
encryptService,
fileUploadService,
configService,
+ stateProvider,
),
deps: [
CryptoServiceAbstraction,
@@ -435,6 +437,7 @@ const safeProviders: SafeProvider[] = [
EncryptService,
CipherFileUploadServiceAbstraction,
ConfigService,
+ StateProvider,
],
}),
safeProvider({
@@ -444,7 +447,6 @@ const safeProviders: SafeProvider[] = [
CryptoServiceAbstraction,
I18nServiceAbstraction,
CipherServiceAbstraction,
- StateServiceAbstraction,
StateProvider,
],
}),
diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts
index ab09d14c86..d29c74b42d 100644
--- a/libs/angular/src/vault/components/add-edit.component.ts
+++ b/libs/angular/src/vault/components/add-edit.component.ts
@@ -1,6 +1,6 @@
import { DatePipe } from "@angular/common";
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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@@ -687,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
async loadAddEditCipherInfo(): Promise {
- const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
+ const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
const loadedSavedInfo = addEditCipherInfo != null;
if (loadedSavedInfo) {
@@ -700,7 +700,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
- await this.stateService.setAddEditCipherInfo(null);
+ await this.cipherService.setAddEditCipherInfo(null);
return loadedSavedInfo;
}
diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts
index fd76cad6d1..2348c8844a 100644
--- a/libs/common/src/platform/abstractions/state.service.ts
+++ b/libs/common/src/platform/abstractions/state.service.ts
@@ -6,10 +6,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username";
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 { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
@@ -38,8 +34,6 @@ export abstract class StateService {
clean: (options?: StorageOptions) => Promise;
init: (initOptions?: InitOptions) => Promise;
- getAddEditCipherInfo: (options?: StorageOptions) => Promise;
- setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise;
/**
* Gets the user's auto key
*/
@@ -104,8 +98,6 @@ export abstract class StateService {
* @deprecated For migration purposes only, use setUserKeyBiometric instead
*/
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise;
- getDecryptedCiphers: (options?: StorageOptions) => Promise;
- setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise;
getDecryptedPasswordGenerationHistory: (
options?: StorageOptions,
) => Promise;
@@ -134,11 +126,6 @@ export abstract class StateService {
value: boolean,
options?: StorageOptions,
) => Promise;
- getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>;
- setEncryptedCiphers: (
- value: { [id: string]: CipherData },
- options?: StorageOptions,
- ) => Promise;
getEncryptedPasswordGenerationHistory: (
options?: StorageOptions,
) => Promise;
@@ -165,11 +152,6 @@ export abstract class StateService {
setLastActive: (value: number, options?: StorageOptions) => Promise;
getLastSync: (options?: StorageOptions) => Promise;
setLastSync: (value: string, options?: StorageOptions) => Promise;
- getLocalData: (options?: StorageOptions) => Promise<{ [cipherId: string]: LocalData }>;
- setLocalData: (
- value: { [cipherId: string]: LocalData },
- options?: StorageOptions,
- ) => Promise;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise;
getOrganizationInvitation: (options?: StorageOptions) => Promise;
diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts
index 759a903514..ae7780ada4 100644
--- a/libs/common/src/platform/models/domain/account.ts
+++ b/libs/common/src/platform/models/domain/account.ts
@@ -8,9 +8,6 @@ import {
} from "../../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options";
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 { Utils } from "../../misc/utils";
@@ -61,28 +58,17 @@ export class DataEncryptionPair {
}
export class AccountData {
- ciphers?: DataEncryptionPair = new DataEncryptionPair<
- CipherData,
- CipherView
- >();
- localData?: any;
passwordGenerationHistory?: EncryptionPair<
GeneratedPasswordHistory[],
GeneratedPasswordHistory[]
> = new EncryptionPair();
- addEditCipherInfo?: AddEditCipherInfo;
static fromJSON(obj: DeepJsonify): AccountData {
if (obj == null) {
return null;
}
- return Object.assign(new AccountData(), obj, {
- addEditCipherInfo: {
- cipher: CipherView.fromJSON(obj?.addEditCipherInfo?.cipher),
- collectionIds: obj?.addEditCipherInfo?.collectionIds,
- },
- });
+ return Object.assign(new AccountData(), obj);
}
}
diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts
index 3d512175a8..9edc9ed1e3 100644
--- a/libs/common/src/platform/services/state.service.ts
+++ b/libs/common/src/platform/services/state.service.ts
@@ -9,10 +9,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username";
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 { LogService } from "../abstractions/log.service";
import {
@@ -221,34 +217,6 @@ export class StateService<
return currentUser as UserId;
}
- async getAddEditCipherInfo(options?: StorageOptions): Promise {
- 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),
- collectionIds: raw?.collectionIds,
- };
- }
-
- async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise {
- 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
*/
@@ -465,24 +433,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
}
- @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
- async getDecryptedCiphers(options?: StorageOptions): Promise {
- return (
- await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
- )?.data?.ciphers?.decrypted;
- }
-
- async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise {
- 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)
async getDecryptedPasswordGenerationHistory(
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 {
- 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
*/
@@ -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 {
- 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 {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -1510,50 +1419,3 @@ function withPrototypeForArrayMembers(
};
};
}
-
-function withPrototypeForObjectValues(
- 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 };
- }
- });
- },
- };
- };
-}
diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts
index 518e5c51d6..18df252062 100644
--- a/libs/common/src/platform/state/state-definitions.ts
+++ b/libs/common/src/platform/state/state-definitions.ts
@@ -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_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");
diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts
index 77b949126f..000f85519e 100644
--- a/libs/common/src/state-migrations/migrate.ts
+++ b/libs/common/src/state-migrations/migrate.ts
@@ -53,6 +53,7 @@ import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-m
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
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 { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
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";
export const MIN_VERSION = 3;
-export const CURRENT_VERSION = 56;
-
+export const CURRENT_VERSION = 57;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -119,7 +119,8 @@ export function createMigrationBuilder() {
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
.with(SendMigrator, 53, 54)
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
- .with(AuthRequestMigrator, 55, CURRENT_VERSION);
+ .with(AuthRequestMigrator, 55, 56)
+ .with(CipherServiceMigrator, 56, CURRENT_VERSION);
}
export async function currentVersion(
diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts
new file mode 100644
index 0000000000..499cff1c89
--- /dev/null
+++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts
@@ -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;
+ 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());
+ });
+ });
+});
diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts
new file mode 100644
index 0000000000..e71d889bb7
--- /dev/null
+++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts
@@ -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 {
+ const accounts = await helper.getAccounts();
+ async function migrateAccount(userId: string, account: ExpectedAccountType): Promise {
+ 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 {
+ const accounts = await helper.getAccounts();
+ async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise {
+ //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))]);
+ }
+}
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index a8a0a25e9b..501fd87665 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -1,3 +1,5 @@
+import { Observable } from "rxjs";
+
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
@@ -7,8 +9,13 @@ import { Cipher } from "../models/domain/cipher";
import { Field } from "../models/domain/field";
import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view";
+import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
export abstract class CipherService {
+ /**
+ * An observable monitoring the add/edit cipher info saved to memory.
+ */
+ addEditCipherInfo$: Observable;
clearCache: (userId?: string) => Promise;
encrypt: (
model: CipherView,
@@ -102,4 +109,5 @@ export abstract class CipherService {
asAdmin?: boolean,
) => Promise;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise;
+ setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise;
}
diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts
index 1452ffe7ee..f8db7186d6 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.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts
index c374724781..28c4bfc653 100644
--- a/libs/common/src/vault/services/cipher.service.spec.ts
+++ b/libs/common/src/vault/services/cipher.service.spec.ts
@@ -1,6 +1,8 @@
import { mock } from "jest-mock-extended";
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 { ApiService } from "../../abstractions/api.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 { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
+import { Utils } from "../../platform/misc/utils";
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 { ContainerService } from "../../platform/services/container.service";
+import { UserId } from "../../types/guid";
import { CipherKey, OrgKey } from "../../types/key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
@@ -97,6 +101,8 @@ const cipherData: CipherData = {
},
],
};
+const mockUserId = Utils.newGuid() as UserId;
+let accountService: FakeAccountService;
describe("Cipher Service", () => {
const cryptoService = mock();
@@ -109,6 +115,8 @@ describe("Cipher Service", () => {
const searchService = mock();
const encryptService = mock();
const configService = mock();
+ accountService = mockAccountServiceWith(mockUserId);
+ const stateProvider = new FakeStateProvider(accountService);
let cipherService: CipherService;
let cipherObj: Cipher;
@@ -130,6 +138,7 @@ describe("Cipher Service", () => {
encryptService,
cipherFileUploadService,
configService,
+ stateProvider,
);
cipherObj = new Cipher(cipherData);
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index dffbf5cbbe..e8544d7f98 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -1,4 +1,4 @@
-import { firstValueFrom } from "rxjs";
+import { Observable, firstValueFrom } from "rxjs";
import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service";
@@ -21,13 +21,15 @@ import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
+import { ActiveUserState, StateProvider } from "../../platform/state";
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
-import { OrgKey, UserKey } from "../../types/key";
+import { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
+import { LocalData } from "../models/data/local.data";
import { Attachment } from "../models/domain/attachment";
import { Card } from "../models/domain/card";
import { Cipher } from "../models/domain/cipher";
@@ -54,6 +56,14 @@ import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view";
import { PasswordHistoryView } from "../models/view/password-history.view";
+import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
+
+import {
+ ENCRYPTED_CIPHERS,
+ LOCAL_DATA_KEY,
+ ADD_EDIT_CIPHER_INFO_KEY,
+ DECRYPTED_CIPHERS,
+} from "./key-state/ciphers.state";
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
@@ -62,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction {
this.sortCiphersByLastUsed,
);
+ localData$: Observable>;
+ ciphers$: Observable>;
+ cipherViews$: Observable>;
+ addEditCipherInfo$: Observable;
+
+ private localDataState: ActiveUserState>;
+ private encryptedCiphersState: ActiveUserState>;
+ private decryptedCiphersState: ActiveUserState>;
+ private addEditCipherInfoState: ActiveUserState;
+
constructor(
private cryptoService: CryptoService,
private domainSettingsService: DomainSettingsService,
@@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction {
private encryptService: EncryptService,
private cipherFileUploadService: CipherFileUploadService,
private configService: ConfigService,
- ) {}
+ private stateProvider: StateProvider,
+ ) {
+ this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
+ this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
+ this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
+ this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
- async getDecryptedCipherCache(): Promise {
- const decryptedCiphers = await this.stateService.getDecryptedCiphers();
- return decryptedCiphers;
+ this.localData$ = this.localDataState.state$;
+ this.ciphers$ = this.encryptedCiphersState.state$;
+ this.cipherViews$ = this.decryptedCiphersState.state$;
+ this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
}
async setDecryptedCipherCache(value: CipherView[]) {
@@ -85,7 +111,7 @@ export class CipherService implements CipherServiceAbstraction {
// if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again.
// We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption.
if (value == null || value.length !== 0) {
- await this.stateService.setDecryptedCiphers(value);
+ await this.setDecryptedCiphers(value);
}
if (this.searchService != null) {
if (value == null) {
@@ -96,6 +122,14 @@ export class CipherService implements CipherServiceAbstraction {
}
}
+ private async setDecryptedCiphers(value: CipherView[]) {
+ const cipherViews: { [id: string]: CipherView } = {};
+ value?.forEach((c) => {
+ cipherViews[c.id] = c;
+ });
+ await this.decryptedCiphersState.update(() => cipherViews);
+ }
+
async clearCache(userId?: string): Promise {
await this.clearDecryptedCiphersState(userId);
}
@@ -268,24 +302,27 @@ 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;
}
- const localData = await this.stateService.getLocalData();
- return new Cipher(ciphers[id], localData ? localData[id] : null);
+ const localData = await firstValueFrom(this.localData$);
+ const cipherId = id as CipherId;
+
+ return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
}
async getAll(): Promise {
- const localData = await this.stateService.getLocalData();
- const ciphers = await this.stateService.getEncryptedCiphers();
+ const localData = await firstValueFrom(this.localData$);
+ const ciphers = await firstValueFrom(this.ciphers$);
const response: Cipher[] = [];
for (const id in ciphers) {
// eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) {
- response.push(new Cipher(ciphers[id], localData ? localData[id] : null));
+ const cipherId = id as CipherId;
+ response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null));
}
}
return response;
@@ -293,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction {
@sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise {
- if ((await this.getDecryptedCipherCache()) != null) {
+ let decCiphers = await this.getDecryptedCiphers();
+ if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers();
- return await this.getDecryptedCipherCache();
+ return await this.getDecryptedCiphers();
}
- const ciphers = await this.getAll();
+ decCiphers = await this.decryptCiphers(await this.getAll());
+
+ await this.setDecryptedCipherCache(decCiphers);
+ return decCiphers;
+ }
+
+ private async getDecryptedCiphers() {
+ return Object.values(await firstValueFrom(this.cipherViews$));
+ }
+
+ private async decryptCiphers(ciphers: Cipher[]) {
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
if (Object.keys(orgKeys).length === 0 && userKey == null) {
@@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction {
.flat()
.sort(this.getLocaleSortingFunction());
- await this.setDecryptedCipherCache(decCiphers);
return decCiphers;
}
@@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
if (reindexRequired) {
- await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
+ await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId);
}
}
@@ -448,22 +495,24 @@ export class CipherService implements CipherServiceAbstraction {
}
async updateLastUsedDate(id: string): Promise {
- let ciphersLocalData = await this.stateService.getLocalData();
+ let ciphersLocalData = await firstValueFrom(this.localData$);
+
if (!ciphersLocalData) {
ciphersLocalData = {};
}
- if (ciphersLocalData[id]) {
- ciphersLocalData[id].lastUsedDate = new Date().getTime();
+ const cipherId = id as CipherId;
+ if (ciphersLocalData[cipherId]) {
+ ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
} else {
- ciphersLocalData[id] = {
+ ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(),
};
}
- await this.stateService.setLocalData(ciphersLocalData);
+ await this.localDataState.update(() => ciphersLocalData);
- const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
+ const decryptedCipherCache = await this.getDecryptedCiphers();
if (!decryptedCipherCache) {
return;
}
@@ -471,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i];
if (cached.id === id) {
- cached.localData = ciphersLocalData[id];
+ cached.localData = ciphersLocalData[id as CipherId];
break;
}
}
- await this.stateService.setDecryptedCiphers(decryptedCipherCache);
+ await this.setDecryptedCiphers(decryptedCipherCache);
}
async updateLastLaunchedDate(id: string): Promise {
- let ciphersLocalData = await this.stateService.getLocalData();
+ let ciphersLocalData = await firstValueFrom(this.localData$);
+
if (!ciphersLocalData) {
ciphersLocalData = {};
}
- if (ciphersLocalData[id]) {
- ciphersLocalData[id].lastLaunched = new Date().getTime();
+ const cipherId = id as CipherId;
+ if (ciphersLocalData[cipherId]) {
+ ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
} else {
- ciphersLocalData[id] = {
+ ciphersLocalData[cipherId] = {
lastUsedDate: new Date().getTime(),
};
}
- await this.stateService.setLocalData(ciphersLocalData);
+ await this.localDataState.update(() => ciphersLocalData);
- const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
+ const decryptedCipherCache = await this.getDecryptedCiphers();
if (!decryptedCipherCache) {
return;
}
@@ -502,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction {
for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i];
if (cached.id === id) {
- cached.localData = ciphersLocalData[id];
+ cached.localData = ciphersLocalData[id as CipherId];
break;
}
}
- await this.stateService.setDecryptedCiphers(decryptedCipherCache);
+ await this.setDecryptedCiphers(decryptedCipherCache);
}
async saveNeverDomain(domain: string): Promise {
@@ -711,7 +762,7 @@ export class CipherService implements CipherServiceAbstraction {
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
// Update the local state
- const ciphers = await this.stateService.getEncryptedCiphers();
+ const ciphers = await firstValueFrom(this.ciphers$);
for (const id of cipherIds) {
const cipher = ciphers[id];
@@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction {
}
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => ciphers);
}
async upsert(cipher: CipherData | CipherData[]): Promise {
- let ciphers = await this.stateService.getEncryptedCiphers();
- if (ciphers == null) {
- ciphers = {};
- }
-
- if (cipher instanceof CipherData) {
- const c = cipher as CipherData;
- ciphers[c.id] = c;
- } else {
- (cipher as CipherData[]).forEach((c) => {
- ciphers[c.id] = c;
- });
- }
-
- await this.replace(ciphers);
+ const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
+ await this.updateEncryptedCipherState((current) => {
+ ciphers.forEach((c) => current[c.id as CipherId]);
+ return current;
+ });
}
async replace(ciphers: { [id: string]: CipherData }): Promise {
+ await this.updateEncryptedCipherState(() => ciphers);
+ }
+
+ private async updateEncryptedCipherState(
+ update: (current: Record) => Record,
+ ) {
await this.clearDecryptedCiphersState();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update((current) => {
+ const result = update(current ?? {});
+ return result;
+ });
}
async clear(userId?: string): Promise {
@@ -762,7 +812,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 = {};
}
@@ -770,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction {
ids.forEach((id) => {
// eslint-disable-next-line
if (ciphers.hasOwnProperty(id)) {
- ciphers[id].folderId = folderId;
+ ciphers[id as CipherId].folderId = folderId;
}
});
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => ciphers);
}
async delete(id: string | string[]): Promise {
- const ciphers = await this.stateService.getEncryptedCiphers();
+ const ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
return;
}
if (typeof id === "string") {
- if (ciphers[id] == null) {
+ const cipherId = id as CipherId;
+ if (ciphers[cipherId] == null) {
return;
}
- delete ciphers[id];
+ delete ciphers[cipherId];
} else {
- (id as string[]).forEach((i) => {
+ (id as CipherId[]).forEach((i) => {
delete ciphers[i];
});
}
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => ciphers);
}
async deleteWithServer(id: string, asAdmin = false): Promise {
@@ -820,21 +871,26 @@ export class CipherService implements CipherServiceAbstraction {
}
async deleteAttachment(id: string, attachmentId: string): Promise {
- const ciphers = await this.stateService.getEncryptedCiphers();
-
+ let ciphers = await firstValueFrom(this.ciphers$);
+ const cipherId = id as CipherId;
// eslint-disable-next-line
- if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) {
+ if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
return;
}
- for (let i = 0; i < ciphers[id].attachments.length; i++) {
- if (ciphers[id].attachments[i].id === attachmentId) {
- ciphers[id].attachments.splice(i, 1);
+ for (let i = 0; i < ciphers[cipherId].attachments.length; i++) {
+ if (ciphers[cipherId].attachments[i].id === attachmentId) {
+ ciphers[cipherId].attachments.splice(i, 1);
}
}
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => {
+ if (ciphers == null) {
+ ciphers = {};
+ }
+ return ciphers;
+ });
}
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise {
@@ -917,12 +973,12 @@ 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;
}
- const setDeletedDate = (cipherId: string) => {
+ const setDeletedDate = (cipherId: CipherId) => {
if (ciphers[cipherId] == null) {
return;
}
@@ -930,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction {
};
if (typeof id === "string") {
- setDeletedDate(id);
+ setDeletedDate(id as CipherId);
} else {
(id as string[]).forEach(setDeletedDate);
}
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => {
+ if (ciphers == null) {
+ ciphers = {};
+ }
+ return ciphers;
+ });
}
async softDeleteWithServer(id: string, asAdmin = false): Promise {
@@ -963,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction {
async restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) {
- const ciphers = await this.stateService.getEncryptedCiphers();
+ let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) {
return;
}
const clearDeletedDate = (c: { id: string; revisionDate: string }) => {
- if (ciphers[c.id] == null) {
+ const cipherId = c.id as CipherId;
+ if (ciphers[cipherId] == null) {
return;
}
- ciphers[c.id].deletedDate = null;
- ciphers[c.id].revisionDate = c.revisionDate;
+ ciphers[cipherId].deletedDate = null;
+ ciphers[cipherId].revisionDate = c.revisionDate;
};
if (cipher.constructor.name === Array.name) {
@@ -983,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction {
}
await this.clearCache();
- await this.stateService.setEncryptedCiphers(ciphers);
+ await this.encryptedCiphersState.update(() => {
+ if (ciphers == null) {
+ ciphers = {};
+ }
+ return ciphers;
+ });
}
async restoreWithServer(id: string, asAdmin = false): Promise {
@@ -1025,6 +1092,10 @@ export class CipherService implements CipherServiceAbstraction {
);
}
+ async setAddEditCipherInfo(value: AddEditCipherInfo) {
+ await this.addEditCipherInfoState.update(() => value);
+ }
+
// Helpers
// In the case of a cipher that is being shared with an organization, we want to decrypt the
@@ -1350,11 +1421,11 @@ export class CipherService implements CipherServiceAbstraction {
}
private async clearEncryptedCiphersState(userId?: string) {
- await this.stateService.setEncryptedCiphers(null, { userId: userId });
+ await this.encryptedCiphersState.update(() => ({}));
}
private async clearDecryptedCiphersState(userId?: string) {
- await this.stateService.setDecryptedCiphers(null, { userId: userId });
+ await this.setDecryptedCiphers(null);
this.clearSortedCiphers();
}
diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts
index 88595720e2..8c3be9abe8 100644
--- a/libs/common/src/vault/services/folder/folder.service.spec.ts
+++ b/libs/common/src/vault/services/folder/folder.service.spec.ts
@@ -8,7 +8,6 @@ import { FakeStateProvider } from "../../../../spec/fake-state-provider";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
-import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -27,7 +26,6 @@ describe("Folder Service", () => {
let encryptService: MockProxy;
let i18nService: MockProxy;
let cipherService: MockProxy;
- let stateService: MockProxy;
let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId;
@@ -39,7 +37,6 @@ describe("Folder Service", () => {
encryptService = mock();
i18nService = mock();
cipherService = mock();
- stateService = mock();
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
@@ -52,13 +49,7 @@ describe("Folder Service", () => {
);
encryptService.decryptToUtf8.mockResolvedValue("DEC");
- folderService = new FolderService(
- cryptoService,
- i18nService,
- cipherService,
- stateService,
- stateProvider,
- );
+ folderService = new FolderService(cryptoService, i18nService, cipherService, stateProvider);
folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS);
diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts
index afe3b01c68..584567aee8 100644
--- a/libs/common/src/vault/services/folder/folder.service.ts
+++ b/libs/common/src/vault/services/folder/folder.service.ts
@@ -2,17 +2,16 @@ import { Observable, firstValueFrom, map } from "rxjs";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
-import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
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 { Folder } from "../../../vault/models/domain/folder";
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";
export class FolderService implements InternalFolderServiceAbstraction {
@@ -26,7 +25,6 @@ export class FolderService implements InternalFolderServiceAbstraction {
private cryptoService: CryptoService,
private i18nService: I18nService,
private cipherService: CipherService,
- private stateService: StateService,
private stateProvider: StateProvider,
) {
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"
- const ciphers = await this.stateService.getEncryptedCiphers();
+ const ciphers = await this.cipherService.getAll();
if (ciphers != null) {
- const updates: CipherData[] = [];
+ const updates: Cipher[] = [];
for (const cId in ciphers) {
if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null;
@@ -156,7 +154,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
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.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.cipherService.upsert(updates);
+ this.cipherService.upsert(updates.map((c) => c.toCipherData()));
}
}
}
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 0000000000..71da4c2333
--- /dev/null
+++ b/libs/common/src/vault/services/key-state/ciphers.state.ts
@@ -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(CIPHERS_DISK, "ciphers", {
+ deserializer: (obj: Jsonify) => CipherData.fromJSON(obj),
+});
+
+export const DECRYPTED_CIPHERS = KeyDefinition.record(
+ CIPHERS_MEMORY,
+ "decryptedCiphers",
+ {
+ deserializer: (cipher: Jsonify) => CipherView.fromJSON(cipher),
+ },
+);
+
+export const LOCAL_DATA_KEY = new KeyDefinition>(
+ CIPHERS_DISK_LOCAL,
+ "localData",
+ {
+ deserializer: (localData) => localData,
+ },
+);
+
+export const ADD_EDIT_CIPHER_INFO_KEY = new KeyDefinition(
+ 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);
+
+ return { cipher, collectionIds: addEditCipherInfo.collectionIds };
+ },
+ },
+);
From cbb7e1840d54882fa67530b04adb0600b852ff79 Mon Sep 17 00:00:00 2001
From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Date: Tue, 16 Apr 2024 19:39:31 +0200
Subject: [PATCH 02/86] [PM-2570] [PM-4649] Update change master password UI
(#8416)
* Update the change master password dialog on browser
Change text to remove the mention of the bitwarden.com web vault
Change icon to show it's external link
Changes based on Figma attached to PM-2570
* Update the change master password dialog on desktop
Change text to remove the mention of the bitwarden.com web vault
Changes based on Figma attached to PM-2570 and to replicate what is done on browser
---------
Co-authored-by: Daniel James Smith
---
apps/browser/src/_locales/en/messages.json | 12 ++++++------
.../src/popup/settings/settings.component.html | 2 +-
.../browser/src/popup/settings/settings.component.ts | 5 +++--
apps/desktop/src/locales/en/messages.json | 7 +++++--
apps/desktop/src/main/menu/menu.account.ts | 8 ++++----
5 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 4108db3996..5e941083df 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -172,6 +172,12 @@
"changeMasterPassword": {
"message": "Change master password"
},
+ "continueToWebApp": {
+ "message": "Continue to web app?"
+ },
+ "changeMasterPasswordOnWebConfirmation": {
+ "message": "You can change your master password on the Bitwarden web app."
+ },
"fingerprintPhrase": {
"message": "Fingerprint phrase",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
@@ -557,12 +563,6 @@
"addedFolder": {
"message": "Folder added"
},
- "changeMasterPass": {
- "message": "Change master password"
- },
- "changeMasterPasswordConfirmation": {
- "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?"
- },
"twoStepLoginConfirmation": {
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
},
diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html
index f099528918..98c218b0db 100644
--- a/apps/browser/src/popup/settings/settings.component.html
+++ b/apps/browser/src/popup/settings/settings.component.html
@@ -153,7 +153,7 @@
*ngIf="showChangeMasterPass"
>
{{ "changeMasterPassword" | i18n }}
-
+
-