[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();
|
||||
editItemSpy = jest.spyOn(notificationBackground as any, "editItem");
|
||||
setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo");
|
||||
setAddEditCipherInfoSpy = jest.spyOn(cipherService, "setAddEditCipherInfo");
|
||||
openAddEditVaultItemPopoutSpy = jest.spyOn(
|
||||
notificationBackground as any,
|
||||
"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.
|
||||
*
|
||||
* @param cipherView - The cipher to edit
|
||||
* @param senderTab - The tab that the message was sent from
|
||||
*/
|
||||
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
|
||||
await this.stateService.setAddEditCipherInfo({
|
||||
await this.cipherService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
|
|
@ -592,7 +592,7 @@ describe("OverlayBackground", () => {
|
|||
beforeEach(() => {
|
||||
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
|
||||
jest
|
||||
.spyOn(overlayBackground["stateService"], "setAddEditCipherInfo")
|
||||
.spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo")
|
||||
.mockImplementation();
|
||||
jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation();
|
||||
});
|
||||
|
@ -600,7 +600,7 @@ describe("OverlayBackground", () => {
|
|||
it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
|
||||
sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender);
|
||||
|
||||
expect(overlayBackground["stateService"].setAddEditCipherInfo).not.toHaveBeenCalled();
|
||||
expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled();
|
||||
expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -621,7 +621,7 @@ describe("OverlayBackground", () => {
|
|||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayBackground["stateService"].setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(BrowserApi.sendMessage).toHaveBeenCalledWith(
|
||||
"inlineAutofillMenuRefreshAddEditCipher",
|
||||
);
|
||||
|
|
|
@ -636,7 +636,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = loginView;
|
||||
|
||||
await this.stateService.setAddEditCipherInfo({
|
||||
await this.cipherService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
|
|
@ -663,12 +663,12 @@ export default class MainBackground {
|
|||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
);
|
||||
this.folderService = new FolderService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.cipherService,
|
||||
this.stateService,
|
||||
this.stateProvider,
|
||||
);
|
||||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
|
@ -42,6 +43,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private stateService: BrowserStateService,
|
||||
private browserSendStateService: BrowserSendStateService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
private cipherService: CipherService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: ForegroundPlatformUtilsService,
|
||||
|
@ -161,7 +163,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
await this.clearComponentStates();
|
||||
}
|
||||
if (url.startsWith("/tabs/")) {
|
||||
await this.stateService.setAddEditCipherInfo(null);
|
||||
await this.cipherService.setAddEditCipherInfo(null);
|
||||
}
|
||||
(window as any).previousPopupUrl = url;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -9,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
|
||||
|
||||
|
@ -19,6 +21,7 @@ import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher
|
|||
export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
private addEditCipherInfo: AddEditCipherInfo;
|
||||
private cipherState: CipherView;
|
||||
private cipherService: CipherService;
|
||||
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
|
@ -26,6 +29,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
stateService: StateService,
|
||||
cipherService: CipherService,
|
||||
route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
private location: Location,
|
||||
|
@ -40,10 +44,11 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||
route,
|
||||
window,
|
||||
);
|
||||
this.cipherService = cipherService;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
|
||||
this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$);
|
||||
if (this.addEditCipherInfo != null) {
|
||||
this.cipherState = this.addEditCipherInfo.cipher;
|
||||
}
|
||||
|
@ -64,7 +69,7 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||
this.addEditCipherInfo.cipher = this.cipherState;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.stateService.setAddEditCipherInfo(this.addEditCipherInfo);
|
||||
this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo);
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
i18nServiceFactory,
|
||||
I18nServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/i18n-service.factory";
|
||||
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
|
@ -81,6 +82,7 @@ export function cipherServiceFactory(
|
|||
await encryptServiceFactory(cache, opts),
|
||||
await cipherFileUploadServiceFactory(cache, opts),
|
||||
await configServiceFactory(cache, opts),
|
||||
await stateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,11 +14,10 @@ import {
|
|||
i18nServiceFactory,
|
||||
I18nServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/i18n-service.factory";
|
||||
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
|
||||
import {
|
||||
stateServiceFactory as stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
stateProviderFactory,
|
||||
StateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
|
||||
|
||||
|
@ -28,7 +27,7 @@ export type FolderServiceInitOptions = FolderServiceFactoryOptions &
|
|||
CryptoServiceInitOptions &
|
||||
CipherServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
StateServiceInitOptions;
|
||||
StateProviderInitOptions;
|
||||
|
||||
export function folderServiceFactory(
|
||||
cache: { folderService?: AbstractFolderService } & CachedServices,
|
||||
|
@ -43,7 +42,6 @@ export function folderServiceFactory(
|
|||
await cryptoServiceFactory(cache, opts),
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await cipherServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await stateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -304,7 +304,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
}
|
||||
|
||||
private saveCipherState() {
|
||||
return this.stateService.setAddEditCipherInfo({
|
||||
return this.cipherService.setAddEditCipherInfo({
|
||||
cipher: this.cipher,
|
||||
collectionIds:
|
||||
this.collections == null
|
||||
|
|
|
@ -544,13 +544,13 @@ export class Main {
|
|||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.folderService = new FolderService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.cipherService,
|
||||
this.stateService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { GeneratorComponent } from "./generator.component";
|
||||
|
||||
|
@ -54,6 +55,10 @@ describe("GeneratorComponent", () => {
|
|||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).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 { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
|
||||
import { Account } from "./account";
|
||||
import { GlobalState } from "./global-state";
|
||||
|
@ -57,19 +56,6 @@ export class StateService extends BaseStateService<GlobalState, 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> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.getLastSync(options);
|
||||
|
|
|
@ -411,6 +411,7 @@ const safeProviders: SafeProvider[] = [
|
|||
encryptService: EncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
stateProvider: StateProvider,
|
||||
) =>
|
||||
new CipherService(
|
||||
cryptoService,
|
||||
|
@ -423,6 +424,7 @@ const safeProviders: SafeProvider[] = [
|
|||
encryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
),
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
|
@ -435,6 +437,7 @@ const safeProviders: SafeProvider[] = [
|
|||
EncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigService,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
@ -444,7 +447,6 @@ const safeProviders: SafeProvider[] = [
|
|||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DatePipe } from "@angular/common";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { concatMap, Observable, Subject, takeUntil } from "rxjs";
|
||||
import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
@ -687,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async loadAddEditCipherInfo(): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
|
||||
const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
|
||||
const loadedSavedInfo = addEditCipherInfo != null;
|
||||
|
||||
if (loadedSavedInfo) {
|
||||
|
@ -700,7 +700,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
await this.stateService.setAddEditCipherInfo(null);
|
||||
await this.cipherService.setAddEditCipherInfo(null);
|
||||
|
||||
return loadedSavedInfo;
|
||||
}
|
||||
|
|
|
@ -6,10 +6,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
|
|||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { LocalData } from "../../vault/models/data/local.data";
|
||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { KdfType } from "../enums";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
|
@ -38,8 +34,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
clean: (options?: StorageOptions) => Promise<UserId>;
|
||||
init: (initOptions?: InitOptions) => Promise<void>;
|
||||
|
||||
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
|
||||
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
||||
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPasswordGenerationHistory: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<GeneratedPasswordHistory[]>;
|
||||
|
@ -134,11 +126,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
value: boolean,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>;
|
||||
setEncryptedCiphers: (
|
||||
value: { [id: string]: CipherData },
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEncryptedPasswordGenerationHistory: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<GeneratedPasswordHistory[]>;
|
||||
|
@ -165,11 +152,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||
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>;
|
||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
|
|
|
@ -8,9 +8,6 @@ import {
|
|||
} from "../../../tools/generator/password";
|
||||
import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options";
|
||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
|
||||
import { KdfType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
|
@ -61,28 +58,17 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
|||
}
|
||||
|
||||
export class AccountData {
|
||||
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<
|
||||
CipherData,
|
||||
CipherView
|
||||
>();
|
||||
localData?: any;
|
||||
passwordGenerationHistory?: EncryptionPair<
|
||||
GeneratedPasswordHistory[],
|
||||
GeneratedPasswordHistory[]
|
||||
> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
||||
addEditCipherInfo?: AddEditCipherInfo;
|
||||
|
||||
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new AccountData(), obj, {
|
||||
addEditCipherInfo: {
|
||||
cipher: CipherView.fromJSON(obj?.addEditCipherInfo?.cipher),
|
||||
collectionIds: obj?.addEditCipherInfo?.collectionIds,
|
||||
},
|
||||
});
|
||||
return Object.assign(new AccountData(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options";
|
|||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { LocalData } from "../../vault/models/data/local.data";
|
||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { EnvironmentService } from "../abstractions/environment.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import {
|
||||
|
@ -221,34 +217,6 @@ export class StateService<
|
|||
return currentUser as UserId;
|
||||
}
|
||||
|
||||
async getAddEditCipherInfo(options?: StorageOptions): Promise<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
|
||||
*/
|
||||
|
@ -465,24 +433,6 @@ export class StateService<
|
|||
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)
|
||||
async getDecryptedPasswordGenerationHistory(
|
||||
options?: StorageOptions,
|
||||
|
@ -621,27 +571,6 @@ export class StateService<
|
|||
);
|
||||
}
|
||||
|
||||
@withPrototypeForObjectValues(CipherData)
|
||||
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
|
||||
)?.data?.ciphers?.encrypted;
|
||||
}
|
||||
|
||||
async setEncryptedCiphers(
|
||||
value: { [id: string]: CipherData },
|
||||
options?: StorageOptions,
|
||||
): Promise<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
|
||||
*/
|
||||
|
@ -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> {
|
||||
return (
|
||||
(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_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 { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
|
||||
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
|
||||
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
|
@ -60,8 +61,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 56;
|
||||
|
||||
export const CURRENT_VERSION = 57;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
|
@ -119,7 +119,8 @@ export function createMigrationBuilder() {
|
|||
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
|
||||
.with(SendMigrator, 53, 54)
|
||||
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
|
||||
.with(AuthRequestMigrator, 55, CURRENT_VERSION);
|
||||
.with(AuthRequestMigrator, 55, 56)
|
||||
.with(CipherServiceMigrator, 56, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
|
|
@ -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 { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
|
@ -7,8 +9,13 @@ import { Cipher } from "../models/domain/cipher";
|
|||
import { Field } from "../models/domain/field";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { FieldView } from "../models/view/field.view";
|
||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||
|
||||
export abstract class CipherService {
|
||||
/**
|
||||
* An observable monitoring the add/edit cipher info saved to memory.
|
||||
*/
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
clearCache: (userId?: string) => Promise<void>;
|
||||
encrypt: (
|
||||
model: CipherView,
|
||||
|
@ -102,4 +109,5 @@ export abstract class CipherService {
|
|||
asAdmin?: boolean,
|
||||
) => Promise<void>;
|
||||
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 { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherResponse } from "../response/cipher.response";
|
||||
|
@ -84,4 +86,8 @@ export class CipherData {
|
|||
this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph));
|
||||
}
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CipherData>) {
|
||||
return Object.assign(new CipherData(), obj);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { makeStaticByteArray } from "../../../spec/utils";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
|
@ -12,10 +14,12 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
|||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "../../platform/services/container.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherKey, OrgKey } from "../../types/key";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
|
@ -97,6 +101,8 @@ const cipherData: CipherData = {
|
|||
},
|
||||
],
|
||||
};
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
describe("Cipher Service", () => {
|
||||
const cryptoService = mock<CryptoService>();
|
||||
|
@ -109,6 +115,8 @@ describe("Cipher Service", () => {
|
|||
const searchService = mock<SearchService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const configService = mock<ConfigService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
let cipherService: CipherService;
|
||||
let cipherObj: Cipher;
|
||||
|
@ -130,6 +138,7 @@ describe("Cipher Service", () => {
|
|||
encryptService,
|
||||
cipherFileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
);
|
||||
|
||||
cipherObj = new Cipher(cipherData);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
|
@ -21,13 +21,15 @@ import Domain from "../../platform/models/domain/domain-base";
|
|||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, StateProvider } from "../../platform/state";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { UserKey, OrgKey } from "../../types/key";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { LocalData } from "../models/data/local.data";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Card } from "../models/domain/card";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
|
@ -54,6 +56,14 @@ import { AttachmentView } from "../models/view/attachment.view";
|
|||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { FieldView } from "../models/view/field.view";
|
||||
import { PasswordHistoryView } from "../models/view/password-history.view";
|
||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||
|
||||
import {
|
||||
ENCRYPTED_CIPHERS,
|
||||
LOCAL_DATA_KEY,
|
||||
ADD_EDIT_CIPHER_INFO_KEY,
|
||||
DECRYPTED_CIPHERS,
|
||||
} from "./key-state/ciphers.state";
|
||||
|
||||
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
|
||||
|
||||
|
@ -62,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
this.sortCiphersByLastUsed,
|
||||
);
|
||||
|
||||
localData$: Observable<Record<CipherId, LocalData>>;
|
||||
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
|
||||
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
|
||||
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
|
||||
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
|
||||
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
|
@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
private encryptService: EncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
|
||||
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
|
||||
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
|
||||
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
|
||||
|
||||
async getDecryptedCipherCache(): Promise<CipherView[]> {
|
||||
const decryptedCiphers = await this.stateService.getDecryptedCiphers();
|
||||
return decryptedCiphers;
|
||||
this.localData$ = this.localDataState.state$;
|
||||
this.ciphers$ = this.encryptedCiphersState.state$;
|
||||
this.cipherViews$ = this.decryptedCiphersState.state$;
|
||||
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
|
||||
}
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[]) {
|
||||
|
@ -85,7 +111,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
// if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again.
|
||||
// We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption.
|
||||
if (value == null || value.length !== 0) {
|
||||
await this.stateService.setDecryptedCiphers(value);
|
||||
await this.setDecryptedCiphers(value);
|
||||
}
|
||||
if (this.searchService != null) {
|
||||
if (value == null) {
|
||||
|
@ -96,6 +122,14 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
}
|
||||
|
||||
private async setDecryptedCiphers(value: CipherView[]) {
|
||||
const cipherViews: { [id: string]: CipherView } = {};
|
||||
value?.forEach((c) => {
|
||||
cipherViews[c.id] = c;
|
||||
});
|
||||
await this.decryptedCiphersState.update(() => cipherViews);
|
||||
}
|
||||
|
||||
async clearCache(userId?: string): Promise<void> {
|
||||
await this.clearDecryptedCiphersState(userId);
|
||||
}
|
||||
|
@ -268,24 +302,27 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
async get(id: string): Promise<Cipher> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localData = await this.stateService.getLocalData();
|
||||
return new Cipher(ciphers[id], localData ? localData[id] : null);
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const cipherId = id as CipherId;
|
||||
|
||||
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Cipher[]> {
|
||||
const localData = await this.stateService.getLocalData();
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
const response: Cipher[] = [];
|
||||
for (const id in ciphers) {
|
||||
// eslint-disable-next-line
|
||||
if (ciphers.hasOwnProperty(id)) {
|
||||
response.push(new Cipher(ciphers[id], localData ? localData[id] : null));
|
||||
const cipherId = id as CipherId;
|
||||
response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
@ -293,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
|
||||
@sequentialize(() => "getAllDecrypted")
|
||||
async getAllDecrypted(): Promise<CipherView[]> {
|
||||
if ((await this.getDecryptedCipherCache()) != null) {
|
||||
let decCiphers = await this.getDecryptedCiphers();
|
||||
if (decCiphers != null && decCiphers.length !== 0) {
|
||||
await this.reindexCiphers();
|
||||
return await this.getDecryptedCipherCache();
|
||||
return await this.getDecryptedCiphers();
|
||||
}
|
||||
|
||||
const ciphers = await this.getAll();
|
||||
decCiphers = await this.decryptCiphers(await this.getAll());
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
private async getDecryptedCiphers() {
|
||||
return Object.values(await firstValueFrom(this.cipherViews$));
|
||||
}
|
||||
|
||||
private async decryptCiphers(ciphers: Cipher[]) {
|
||||
const orgKeys = await this.cryptoService.getOrgKeys();
|
||||
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
|
||||
if (Object.keys(orgKeys).length === 0 && userKey == null) {
|
||||
|
@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
|
@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
this.searchService != null &&
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -448,22 +495,24 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
async updateLastUsedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
|
||||
if (ciphersLocalData[id]) {
|
||||
ciphersLocalData[id].lastUsedDate = new Date().getTime();
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphersLocalData[cipherId]) {
|
||||
ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
|
||||
} else {
|
||||
ciphersLocalData[id] = {
|
||||
ciphersLocalData[cipherId] = {
|
||||
lastUsedDate: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
|
@ -471,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
for (let i = 0; i < decryptedCipherCache.length; i++) {
|
||||
const cached = decryptedCipherCache[i];
|
||||
if (cached.id === id) {
|
||||
cached.localData = ciphersLocalData[id];
|
||||
cached.localData = ciphersLocalData[id as CipherId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
|
||||
await this.setDecryptedCiphers(decryptedCipherCache);
|
||||
}
|
||||
|
||||
async updateLastLaunchedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
|
||||
if (ciphersLocalData[id]) {
|
||||
ciphersLocalData[id].lastLaunched = new Date().getTime();
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphersLocalData[cipherId]) {
|
||||
ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
|
||||
} else {
|
||||
ciphersLocalData[id] = {
|
||||
ciphersLocalData[cipherId] = {
|
||||
lastUsedDate: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
|
@ -502,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
for (let i = 0; i < decryptedCipherCache.length; i++) {
|
||||
const cached = decryptedCipherCache[i];
|
||||
if (cached.id === id) {
|
||||
cached.localData = ciphersLocalData[id];
|
||||
cached.localData = ciphersLocalData[id as CipherId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
|
||||
await this.setDecryptedCiphers(decryptedCipherCache);
|
||||
}
|
||||
|
||||
async saveNeverDomain(domain: string): Promise<void> {
|
||||
|
@ -711,7 +762,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
|
||||
|
||||
// Update the local state
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
|
||||
for (const id of cipherIds) {
|
||||
const cipher = ciphers[id];
|
||||
|
@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async upsert(cipher: CipherData | CipherData[]): Promise<any> {
|
||||
let ciphers = await this.stateService.getEncryptedCiphers();
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
|
||||
if (cipher instanceof CipherData) {
|
||||
const c = cipher as CipherData;
|
||||
ciphers[c.id] = c;
|
||||
} else {
|
||||
(cipher as CipherData[]).forEach((c) => {
|
||||
ciphers[c.id] = c;
|
||||
});
|
||||
}
|
||||
|
||||
await this.replace(ciphers);
|
||||
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
|
||||
await this.updateEncryptedCipherState((current) => {
|
||||
ciphers.forEach((c) => current[c.id as CipherId]);
|
||||
return current;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
|
||||
await this.updateEncryptedCipherState(() => ciphers);
|
||||
}
|
||||
|
||||
private async updateEncryptedCipherState(
|
||||
update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>,
|
||||
) {
|
||||
await this.clearDecryptedCiphersState();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update((current) => {
|
||||
const result = update(current ?? {});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
|
@ -762,7 +812,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
|
||||
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
|
||||
|
||||
let ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
|
@ -770,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
ids.forEach((id) => {
|
||||
// eslint-disable-next-line
|
||||
if (ciphers.hasOwnProperty(id)) {
|
||||
ciphers[id].folderId = folderId;
|
||||
ciphers[id as CipherId].folderId = folderId;
|
||||
}
|
||||
});
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof id === "string") {
|
||||
if (ciphers[id] == null) {
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
delete ciphers[id];
|
||||
delete ciphers[cipherId];
|
||||
} else {
|
||||
(id as string[]).forEach((i) => {
|
||||
(id as CipherId[]).forEach((i) => {
|
||||
delete ciphers[i];
|
||||
});
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async deleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
|
@ -820,21 +871,26 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
async deleteAttachment(id: string, attachmentId: string): Promise<void> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
const cipherId = id as CipherId;
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) {
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < ciphers[id].attachments.length; i++) {
|
||||
if (ciphers[id].attachments[i].id === attachmentId) {
|
||||
ciphers[id].attachments.splice(i, 1);
|
||||
for (let i = 0; i < ciphers[cipherId].attachments.length; i++) {
|
||||
if (ciphers[cipherId].attachments[i].id === attachmentId) {
|
||||
ciphers[cipherId].attachments.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> {
|
||||
|
@ -917,12 +973,12 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
async softDelete(id: string | string[]): Promise<any> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setDeletedDate = (cipherId: string) => {
|
||||
const setDeletedDate = (cipherId: CipherId) => {
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -930,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
};
|
||||
|
||||
if (typeof id === "string") {
|
||||
setDeletedDate(id);
|
||||
setDeletedDate(id as CipherId);
|
||||
} else {
|
||||
(id as string[]).forEach(setDeletedDate);
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
|
@ -963,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
async restore(
|
||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
||||
) {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearDeletedDate = (c: { id: string; revisionDate: string }) => {
|
||||
if (ciphers[c.id] == null) {
|
||||
const cipherId = c.id as CipherId;
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
ciphers[c.id].deletedDate = null;
|
||||
ciphers[c.id].revisionDate = c.revisionDate;
|
||||
ciphers[cipherId].deletedDate = null;
|
||||
ciphers[cipherId].revisionDate = c.revisionDate;
|
||||
};
|
||||
|
||||
if (cipher.constructor.name === Array.name) {
|
||||
|
@ -983,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async restoreWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
|
@ -1025,6 +1092,10 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
async setAddEditCipherInfo(value: AddEditCipherInfo) {
|
||||
await this.addEditCipherInfoState.update(() => value);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// In the case of a cipher that is being shared with an organization, we want to decrypt the
|
||||
|
@ -1350,11 +1421,11 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
}
|
||||
|
||||
private async clearEncryptedCiphersState(userId?: string) {
|
||||
await this.stateService.setEncryptedCiphers(null, { userId: userId });
|
||||
await this.encryptedCiphersState.update(() => ({}));
|
||||
}
|
||||
|
||||
private async clearDecryptedCiphersState(userId?: string) {
|
||||
await this.stateService.setDecryptedCiphers(null, { userId: userId });
|
||||
await this.setDecryptedCiphers(null);
|
||||
this.clearSortedCiphers();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { FakeStateProvider } from "../../../../spec/fake-state-provider";
|
|||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
@ -27,7 +26,6 @@ describe("Folder Service", () => {
|
|||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
@ -39,7 +37,6 @@ describe("Folder Service", () => {
|
|||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
cipherService = mock<CipherService>();
|
||||
stateService = mock<StateService>();
|
||||
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
@ -52,13 +49,7 @@ describe("Folder Service", () => {
|
|||
);
|
||||
encryptService.decryptToUtf8.mockResolvedValue("DEC");
|
||||
|
||||
folderService = new FolderService(
|
||||
cryptoService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
stateService,
|
||||
stateProvider,
|
||||
);
|
||||
folderService = new FolderService(cryptoService, i18nService, cipherService, stateProvider);
|
||||
|
||||
folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS);
|
||||
|
||||
|
|
|
@ -2,17 +2,16 @@ import { Observable, firstValueFrom, map } from "rxjs";
|
|||
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { Folder } from "../../../vault/models/domain/folder";
|
||||
import { FolderView } from "../../../vault/models/view/folder.view";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state";
|
||||
|
||||
export class FolderService implements InternalFolderServiceAbstraction {
|
||||
|
@ -26,7 +25,6 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS);
|
||||
|
@ -144,9 +142,9 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||
});
|
||||
|
||||
// Items in a deleted folder are re-assigned to "No Folder"
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await this.cipherService.getAll();
|
||||
if (ciphers != null) {
|
||||
const updates: CipherData[] = [];
|
||||
const updates: Cipher[] = [];
|
||||
for (const cId in ciphers) {
|
||||
if (ciphers[cId].folderId === id) {
|
||||
ciphers[cId].folderId = null;
|
||||
|
@ -156,7 +154,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||
if (updates.length > 0) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.cipherService.upsert(updates);
|
||||
this.cipherService.upsert(updates.map((c) => c.toCipherData()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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