[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:
parent
62ed7e5abc
commit
06acdefa91
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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",
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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))]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
Loading…
Reference in New Issue