From f90b3456d5b3782c44c55a49b125ebda0b1cddc4 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 13 Dec 2021 11:15:16 -0500 Subject: [PATCH] [Account Switching] [Feature] Allow clients to store data for more than one user (#491) * [refactor] Extract, rename, and expand StorageServiceOptions * Pulled StorageServiceOptions into its own file * Renamed StorageServiceOptions to StorageOptions * Pulled KeySuffixOpptions into its own file * Converted KeySuffixOptions into an enum from a union type * [refactor] Expand StateService into a full coverage storage proxy * Expand StateService to allow it to manage all data points of the application state regardless of memory. * Expand StateService to allow for storing and managing multiple accounts * [refactor] Create helper services for managing organization and provider state data * [refactor] Implement StateService across service layer * Remove service level variables used for in memory data storage and replaced with calls to StateService * Remove direct calls to StorageService in favor of using StateService as a proxy * [feature] Implement account switching capable services across components and processes * Replace calls to StorageService and deprecated services with calls to a StateService * [chore] Remove unused services Several services are no longer in use because of the expanded state service. These have simply been removed. * [bug] Add loginRedirect to the account model * [bug] Add awaits to newly async calls in TokenService * [bug] Add several missing awaits * [bug] Add state service handlers for AutoConfirmFingerprint * [bug] Move TwoFactorToken to global state * Update unauth-guard.service.ts Add back return true * [refactor] Slim down the boilerplate needed to manage options on StateService calls * [bug] Allow the lock message handler to manipulate a specific acount * [bug] Add missing await to auth guard * [bug] Adjust state scope of several biometric data points * [bug] Ensure vault locking logic can operate over non-active accounts * [style] Fix lint complaints * [bug] Move disableFavicon to global state * [refactor] Remove an unecassary parameter from a StorageOptions instance * [bug] Ensure HtmlStorageService paths are accounted for in StateService * [feature] Add a server url helper to the account model for the account switcher * [refactor] Remove some unused getters from the account model * [bug] Ensure locking and logging out can function over any user * Fix account getting set to null in getAccountFromDisk * [bug] Ensure lock component is always working with the latest active account in state * [chore] Update recent KeyConnector changes to use stateService * [style] Fix lint complaints * [chore] Resolve TokenService merge issues from KeyConnector * [bug] Add missing service arguement * [bug] Correct several default storage option types * [bug] Check for the right key in hasEncKey * [bug] Add enableFullWidth to the account model * [style] Fix lint complaints * [review] Revist remember email * [refactor] Remove RememberEmail from state * setDisableFavicon to correct storage location * [bug] Convert vault lock loop returns into continues to not skip secondary accounts * [review] Sorted state service methods * [bug] Correct neverDomains type on the account model * [review] Rename stateService.purge to stateService.clean * [review] [refactor] Extract lock refresh logic to a load function * [review] [refactor] Extract some timeout logic to dedicated functions * [review] [refactor] Move AuthenticationStatus to a dedicated file * [review] [refactor] Rename Globals to GlobalState * [style] Fix lint complaints * [review] Remove unused global state property for decodedToken * [review] [bug] Adjust state scope for OrganizationInvitation * [review] [bug] Put back the homepage variable in lock guard * [review] Un-try-catch the window creation function * Revert "[review] [bug] Adjust state scope for OrganizationInvitation" This reverts commit caa4574a65d9d0c3573a7529ed2221764fd55497. * [bug] Change || to && in recent vault timeout refactor * [bug] Keep up with entire state in storage instead of just accounts and globals Not having access to the last active user was creating issues across clients when restarting the process. For example: when refreshing the page on web we no longer maintain an understanding of who is logged in. To resolve this I converted all storage save operations to get and save an entire state object, instead of specifying accounts and globals. This allows for more flexible saving, like saving activeUserId as a top level storage item. * [style] Fix lint complaints * Revert "[bug] Keep up with entire state in storage instead of just accounts and globals" This reverts commit e8970725be472386358c1e2f06f53663c4979e0e. * [bug] Initialize GlobalState by default * [bug] Only get key hash from storage * [bug] Remove settings storage location overrides * [bug] Only save accessToken to storage * [refactor] Remove unecassary argements from electron crypto state calls * [bug] Ensure keys and tokens load and save to the right locations for web * [style] Fix lint complaints * [bug] Remove keySuffix storage option and split uses into unique methods The keySuffix options don't work with saving serialized json as a storage object - use cases simply overwrite each other in state. This commit breaks Auto and Biometric keys into distinct storage items and adjusts logic accordingly. * [bug] Add default vault timeouts to new accounts * [bug] Save appId as a top level storage item * [bug] Add missing await to timeout logic * [bug] Adjust state scope for everBeenUnlocked * [bug] Clear access tokens when loading account state from disk * [bug] Adjust theme to be a global state item * [bug] Adjust null checking for window in state * [bug] Correct getGlobals not pulling from the stored state item * [bug] Null check in memory account before claiming it has a userId * [bug] Scaffold secure storage service when building storage objects on init * [bug] Adjusted state scope of event collection * [bug] Adjusted state scope of vault timeout and action * [bug] Grab account from normal storage if secure storage is requested but does not exist * [bug] Create a State if one is requested from memory before it exists * [bug] Ensure all storage locations are cleared on state clean * [style] Fix lint complaints * [bug] Remove uneeded clearing of access token * [bug] Reset tokens when toggling * [refactor] Split up the Account model Until this point the account model has been very flat, holding many kinds of data. In order to be able to prune data at appropriate times, for example clearing keys at logout without clearing QoL settings like locale, the Account model has been divided into logical chunks. * [bug] Correct the serverUrl helpers return * Fix sends always coming back as empty in browser * Get settings properly (I think) * [bug] Fix lint error * [bug] Add missing await to identity token refresh This was causing weird behavior in web that was creating a lot of 429s * [bug] Scaffold memory storage for web Not properly creating storage objects on signin was creating weird behavior when logging out, locking, and logging back in. Namely, encrypted data that was recently synced had nowhere to save to and was lost. * [bug] Implement better null handling in a few places for retrieving state * [bug] Update correct storage locations on account removal * [bug] Added missing awaits to lock component * [bug] Reload lock component on account switching vs. account update * [bug] Store master keys correctly * [bug] Move some biometrics storage items to global state * [feature] Add platform helper isMac() * [refactor] Comment emphasis and call order refresh * [refactor] Remove unecassary using * [bug] Relocate authenticationStatus check logic to component * [bug] Stop not clearing everything on state clean * [style] Fix lint complaints * [bug] Correct mismatched uses of encrypted and decrypted pin states * Add browser specific state classes and methods * lint fixes * [bug] Migrate existing persistant data to new schema * [style] Fix lint complaints * [bug] Dont clear settings on state clean * [bug] Maintain the right storage items on logout * [chore] resolve issues from merge * [bug] Resolve settings clearing on lock * [chore] Added a comment * [review] fromatting for code review * Revert browser state items Co-authored-by: Robyn MacCallum Co-authored-by: Robyn MacCallum --- angular/src/components/add-edit.component.ts | 20 +- .../src/components/attachments.component.ts | 10 +- angular/src/components/avatar.component.ts | 2 +- .../components/change-password.component.ts | 16 +- angular/src/components/groupings.component.ts | 16 +- angular/src/components/icon.component.ts | 6 +- angular/src/components/lock.component.ts | 80 +- angular/src/components/login.component.ts | 32 +- angular/src/components/premium.component.ts | 9 +- angular/src/components/register.component.ts | 2 +- .../components/remove-password.component.ts | 11 +- .../src/components/send/add-edit.component.ts | 12 +- angular/src/components/send/send.component.ts | 4 +- .../src/components/set-password.component.ts | 21 +- angular/src/components/set-pin.component.ts | 24 +- angular/src/components/share.component.ts | 8 +- angular/src/components/sso.component.ts | 27 +- .../src/components/two-factor.component.ts | 9 +- .../update-temp-password.component.ts | 16 +- angular/src/components/view.component.ts | 10 +- angular/src/services/auth-guard.service.ts | 9 +- angular/src/services/jslib-services.module.ts | 229 ++- angular/src/services/lock-guard.service.ts | 27 +- angular/src/services/unauth-guard.service.ts | 10 +- common/src/abstractions/apiKey.service.ts | 9 - common/src/abstractions/cipher.service.ts | 4 +- common/src/abstractions/collection.service.ts | 4 +- common/src/abstractions/crypto.service.ts | 29 +- common/src/abstractions/event.service.ts | 4 +- common/src/abstractions/folder.service.ts | 4 +- .../src/abstractions/organization.service.ts | 11 + .../passwordGeneration.service.ts | 2 +- common/src/abstractions/policy.service.ts | 8 +- common/src/abstractions/provider.service.ts | 9 + common/src/abstractions/send.service.ts | 4 +- common/src/abstractions/settings.service.ts | 4 +- common/src/abstractions/state.service.ts | 261 ++- .../abstractions/stateMigration.service.ts | 4 + common/src/abstractions/storage.service.ts | 15 +- common/src/abstractions/sync.service.ts | 2 +- common/src/abstractions/system.service.ts | 4 +- common/src/abstractions/token.service.ts | 27 +- common/src/abstractions/user.service.ts | 34 - .../src/abstractions/vaultTimeout.service.ts | 13 +- common/src/enums/authenticationStatus.ts | 6 + common/src/enums/htmlStorageLocation.ts | 5 + common/src/enums/keySuffixOptions.ts | 4 + common/src/enums/storageLocation.ts | 5 + common/src/models/domain/account.ts | 168 ++ common/src/models/domain/globalState.ts | 24 + common/src/models/domain/state.ts | 9 + common/src/models/domain/storageOptions.ts | 10 + common/src/services/api.service.ts | 4 +- common/src/services/apiKey.service.ts | 86 - common/src/services/auth.service.ts | 48 +- common/src/services/cipher.service.ts | 201 +-- common/src/services/collection.service.ts | 60 +- common/src/services/constants.service.ts | 68 - common/src/services/crypto.service.ts | 314 ++-- common/src/services/environment.service.ts | 10 +- common/src/services/event.service.ts | 32 +- common/src/services/folder.service.ts | 68 +- common/src/services/keyConnector.service.ts | 30 +- common/src/services/notifications.service.ts | 16 +- common/src/services/organization.service.ts | 49 + .../services/passwordGeneration.service.ts | 56 +- common/src/services/policy.service.ts | 58 +- common/src/services/provider.service.ts | 35 + common/src/services/send.service.ts | 71 +- common/src/services/settings.service.ts | 28 +- common/src/services/state.service.ts | 1580 ++++++++++++++++- common/src/services/stateMigration.service.ts | 335 ++++ common/src/services/sync.service.ts | 77 +- common/src/services/system.service.ts | 24 +- common/src/services/token.service.ts | 180 +- common/src/services/totp.service.ts | 10 +- common/src/services/user.service.ts | 238 --- common/src/services/vaultTimeout.service.ts | 175 +- electron/src/biometric.darwin.main.ts | 15 +- electron/src/biometric.windows.main.ts | 14 +- electron/src/electronConstants.ts | 14 - .../src/services/electronCrypto.service.ts | 41 +- .../services/electronPlatformUtils.service.ts | 14 +- .../electronRendererSecureStorage.service.ts | 12 +- electron/src/tray.main.ts | 18 +- electron/src/utils.ts | 6 +- electron/src/window.main.ts | 49 +- node/src/cli/baseProgram.ts | 12 +- node/src/cli/commands/login.command.ts | 10 +- spec/common/services/cipher.service.spec.ts | 15 +- 90 files changed, 3644 insertions(+), 1722 deletions(-) delete mode 100644 common/src/abstractions/apiKey.service.ts create mode 100644 common/src/abstractions/organization.service.ts create mode 100644 common/src/abstractions/provider.service.ts create mode 100644 common/src/abstractions/stateMigration.service.ts delete mode 100644 common/src/abstractions/user.service.ts create mode 100644 common/src/enums/authenticationStatus.ts create mode 100644 common/src/enums/htmlStorageLocation.ts create mode 100644 common/src/enums/keySuffixOptions.ts create mode 100644 common/src/enums/storageLocation.ts create mode 100644 common/src/models/domain/account.ts create mode 100644 common/src/models/domain/globalState.ts create mode 100644 common/src/models/domain/state.ts create mode 100644 common/src/models/domain/storageOptions.ts delete mode 100644 common/src/services/apiKey.service.ts delete mode 100644 common/src/services/constants.service.ts create mode 100644 common/src/services/organization.service.ts create mode 100644 common/src/services/provider.service.ts create mode 100644 common/src/services/stateMigration.service.ts delete mode 100644 common/src/services/user.service.ts delete mode 100644 electron/src/electronConstants.ts diff --git a/angular/src/components/add-edit.component.ts b/angular/src/components/add-edit.component.ts index edb0b4aede..d8d9a8e318 100644 --- a/angular/src/components/add-edit.component.ts +++ b/angular/src/components/add-edit.component.ts @@ -22,11 +22,11 @@ import { FolderService } from 'jslib-common/abstractions/folder.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { OrganizationService } from 'jslib-common/abstractions/organization.service'; import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { Cipher } from 'jslib-common/models/domain/cipher'; @@ -89,10 +89,10 @@ export class AddEditComponent implements OnInit { constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected stateService: StateService, - protected userService: UserService, protected collectionService: CollectionService, - protected messagingService: MessagingService, protected eventService: EventService, - protected policyService: PolicyService, protected passwordRepromptService: PasswordRepromptService, - private logService: LogService) { + protected collectionService: CollectionService, protected messagingService: MessagingService, + protected eventService: EventService, protected policyService: PolicyService, + private logService: LogService, protected passwordRepromptService: PasswordRepromptService, + private organizationService: OrganizationService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -160,11 +160,11 @@ export class AddEditComponent implements OnInit { if (await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)) { this.allowPersonal = false; } else { - const myEmail = await this.userService.getEmail(); + const myEmail = await this.stateService.getEmail(); this.ownershipOptions.push({ name: myEmail, value: null }); } - const orgs = await this.userService.getAllOrganizations(); + const orgs = await this.organizationService.getAll(); orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach(o => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { this.ownershipOptions.push({ name: o.name, value: o.id }); @@ -193,12 +193,12 @@ export class AddEditComponent implements OnInit { this.title = this.i18nService.t('addItem'); } - const addEditCipherInfo: any = await this.stateService.get('addEditCipherInfo'); + const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); if (addEditCipherInfo != null) { this.cipher = addEditCipherInfo.cipher; this.collectionIds = addEditCipherInfo.collectionIds; } - await this.stateService.remove('addEditCipherInfo'); + await this.stateService.setAddEditCipherInfo(null); if (this.cipher == null) { if (this.editMode) { @@ -442,7 +442,7 @@ export class AddEditComponent implements OnInit { } if (this.cipher.organizationId != null) { this.collections = this.writeableCollections.filter(c => c.organizationId === this.cipher.organizationId); - const org = await this.userService.getOrganization(this.cipher.organizationId); + const org = await this.organizationService.get(this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; } diff --git a/angular/src/components/attachments.component.ts b/angular/src/components/attachments.component.ts index dcbaf19b3b..6745251b2b 100644 --- a/angular/src/components/attachments.component.ts +++ b/angular/src/components/attachments.component.ts @@ -12,7 +12,7 @@ import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { Cipher } from 'jslib-common/models/domain/cipher'; import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; @@ -37,9 +37,9 @@ export class AttachmentsComponent implements OnInit { emergencyAccessId?: string = null; constructor(protected cipherService: CipherService, protected i18nService: I18nService, - protected cryptoService: CryptoService, protected userService: UserService, - protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected win: Window, private logService: LogService) { } + protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, + protected apiService: ApiService, protected win: Window, + protected logService: LogService, protected stateService: StateService) { } async ngOnInit() { await this.init(); @@ -164,7 +164,7 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt(); this.hasUpdatedKey = await this.cryptoService.hasEncKey(); - const canAccessPremium = await this.userService.canAccessPremium(); + const canAccessPremium = await this.stateService.getCanAccessPremium(); this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; if (!this.canAccessAttachments) { diff --git a/angular/src/components/avatar.component.ts b/angular/src/components/avatar.component.ts index 1760c521f2..a9ba91179d 100644 --- a/angular/src/components/avatar.component.ts +++ b/angular/src/components/avatar.component.ts @@ -45,7 +45,7 @@ export class AvatarComponent implements OnChanges, OnInit { } private async generate() { - const enableGravatars = await this.stateService.get('enableGravatars'); + const enableGravatars = await this.stateService.getEnableGravitars(); if (enableGravatars && this.email != null) { const hashBytes = await this.cryptoFunctionService.hash(this.email.toLowerCase().trim(), 'md5'); const hash = Utils.fromBufferToHex(hashBytes).toLowerCase(); diff --git a/angular/src/components/change-password.component.ts b/angular/src/components/change-password.component.ts index 5af79115d7..7bbb3c513a 100644 --- a/angular/src/components/change-password.component.ts +++ b/angular/src/components/change-password.component.ts @@ -6,7 +6,7 @@ import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { EncString } from 'jslib-common/models/domain/encString'; import { MasterPasswordPolicyOptions } from 'jslib-common/models/domain/masterPasswordPolicyOptions'; @@ -29,12 +29,12 @@ export class ChangePasswordComponent implements OnInit { private masterPasswordStrengthTimeout: any; constructor(protected i18nService: I18nService, protected cryptoService: CryptoService, - protected messagingService: MessagingService, protected userService: UserService, - protected passwordGenerationService: PasswordGenerationService, - protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService) { } + protected messagingService: MessagingService, protected passwordGenerationService: PasswordGenerationService, + protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, + protected stateService: StateService) { } async ngOnInit() { - this.email = await this.userService.getEmail(); + this.email = await this.stateService.getEmail(); this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); } @@ -47,12 +47,12 @@ export class ChangePasswordComponent implements OnInit { return; } - const email = await this.userService.getEmail(); + const email = await this.stateService.getEmail(); if (this.kdf == null) { - this.kdf = await this.userService.getKdf(); + this.kdf = await this.stateService.getKdfType(); } if (this.kdfIterations == null) { - this.kdfIterations = await this.userService.getKdfIterations(); + this.kdfIterations = await this.stateService.getKdfIterations(); } const key = await this.cryptoService.makeKey(this.masterPassword, email.trim().toLowerCase(), this.kdf, this.kdfIterations); diff --git a/angular/src/components/groupings.component.ts b/angular/src/components/groupings.component.ts index 1c3c27a1d7..98784bb4b3 100644 --- a/angular/src/components/groupings.component.ts +++ b/angular/src/components/groupings.component.ts @@ -14,10 +14,7 @@ import { TreeNode } from 'jslib-common/models/domain/treeNode'; import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; @Directive() export class GroupingsComponent { @@ -50,15 +47,12 @@ export class GroupingsComponent { selectedCollectionId: string = null; private collapsedGroupings: Set; - private collapsedGroupingsKey: string; constructor(protected collectionService: CollectionService, protected folderService: FolderService, - protected storageService: StorageService, protected userService: UserService) { } + protected stateService: StateService) { } async load(setLoaded = true) { - const userId = await this.userService.getUserId(); - this.collapsedGroupingsKey = ConstantsService.collapsedGroupingsKey + '_' + userId; - const collapsedGroupings = await this.storageService.get(this.collapsedGroupingsKey); + const collapsedGroupings = await this.stateService.getCollapsedGroupings(); if (collapsedGroupings == null) { this.collapsedGroupings = new Set(); } else { @@ -149,7 +143,7 @@ export class GroupingsComponent { this.selectedCollectionId = null; } - collapse(grouping: FolderView | CollectionView, idPrefix = '') { + async collapse(grouping: FolderView | CollectionView, idPrefix = '') { if (grouping.id == null) { return; } @@ -159,7 +153,7 @@ export class GroupingsComponent { } else { this.collapsedGroupings.add(id); } - this.storageService.save(this.collapsedGroupingsKey, this.collapsedGroupings); + await this.stateService.setCollapsedGroupings(this.collapsedGroupings); } isCollapsed(grouping: FolderView | CollectionView, idPrefix = '') { diff --git a/angular/src/components/icon.component.ts b/angular/src/components/icon.component.ts index b3a75d5a8f..1ae988c797 100644 --- a/angular/src/components/icon.component.ts +++ b/angular/src/components/icon.component.ts @@ -11,8 +11,6 @@ import { CipherView } from 'jslib-common/models/view/cipherView'; import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { Utils } from 'jslib-common/misc/utils'; const IconMap: any = { @@ -37,7 +35,7 @@ export class IconComponent implements OnChanges { private iconsUrl: string; - constructor(environmentService: EnvironmentService, protected stateService: StateService) { + constructor(environmentService: EnvironmentService, private stateService: StateService) { this.iconsUrl = environmentService.getIconsUrl(); } @@ -46,7 +44,7 @@ export class IconComponent implements OnChanges { // to avoid this we reset all state variables. this.image = null; this.fallbackImage = null; - this.imageEnabled = !(await this.stateService.get(ConstantsService.disableFaviconKey)); + this.imageEnabled = !(await this.stateService.getDisableFavicon()); this.load(); } diff --git a/angular/src/components/lock.component.ts b/angular/src/components/lock.component.ts index b192ce6c4a..ae9887c2aa 100644 --- a/angular/src/components/lock.component.ts +++ b/angular/src/components/lock.component.ts @@ -11,12 +11,8 @@ import { LogService } from 'jslib-common/abstractions/log.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { EncString } from 'jslib-common/models/domain/encString'; import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; @@ -25,6 +21,7 @@ import { SecretVerificationRequest } from 'jslib-common/models/request/secretVer import { Utils } from 'jslib-common/misc/utils'; import { HashPurpose } from 'jslib-common/enums/hashPurpose'; +import { KeySuffixOptions } from 'jslib-common/enums/keySuffixOptions'; @Directive() export class LockComponent implements OnInit { @@ -41,38 +38,22 @@ export class LockComponent implements OnInit { hideInput: boolean; protected successRoute: string = 'vault'; - protected onSuccessfulSubmit: () => void; + protected onSuccessfulSubmit: () => Promise; private invalidPinAttempts = 0; private pinSet: [boolean, boolean]; constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, - protected userService: UserService, protected cryptoService: CryptoService, - protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, + protected cryptoService: CryptoService, protected vaultTimeoutService: VaultTimeoutService, protected environmentService: EnvironmentService, protected stateService: StateService, protected apiService: ApiService, private logService: LogService, private keyConnectorService: KeyConnectorService, protected ngZone: NgZone) { } async ngOnInit() { - this.pinSet = await this.vaultTimeoutService.isPinLockSet(); - this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && - (await this.cryptoService.hasKeyStored('biometric') || !this.platformUtilsService.supportsSecureStorage()); - this.biometricText = await this.storageService.get(ConstantsService.biometricText); - this.email = await this.userService.getEmail(); - const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); - this.hideInput = usesKeyConnector && !this.pinLock; - - // Users with key connector and without biometric or pin has no MP to unlock using - if (usesKeyConnector && !(this.biometricLock || this.pinLock)) { - await this.vaultTimeoutService.logOut(); - } - - const webVaultUrl = this.environmentService.getWebVaultUrl(); - const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl; - this.webVaultHostname = Utils.getHostname(vaultUrl); + this.stateService.activeAccount.subscribe(async _userId => { + await this.load(); + }); } async submit() { @@ -87,17 +68,17 @@ export class LockComponent implements OnInit { return; } - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); if (this.pinLock) { let failed = true; try { if (this.pinSet[0]) { const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations, - this.vaultTimeoutService.pinProtectedKey); + await this.stateService.getDecryptedPinProtected()); const encKey = await this.cryptoService.getEncKey(key); - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); + const protectedPin = await this.stateService.getProtectedPin(); const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey); failed = decPin !== this.pin; if (!failed) { @@ -148,13 +129,13 @@ export class LockComponent implements OnInit { if (passwordValid) { if (this.pinSet[0]) { - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); + const protectedPin = await this.stateService.getProtectedPin(); const encKey = await this.cryptoService.getEncKey(key); const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey); const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfIterations); - this.vaultTimeoutService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); + await this.stateService.setDecryptedPinProtected(await this.cryptoService.encrypt(key.key, pinKey)); } - this.setKeyAndContinue(key); + await this.setKeyAndContinue(key); } else { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); @@ -175,7 +156,7 @@ export class LockComponent implements OnInit { return; } - const success = (await this.cryptoService.getKey('biometric')) != null; + const success = (await this.cryptoService.getKey(KeySuffixOptions.Biometric)) != null; if (success) { await this.doContinue(); @@ -196,19 +177,40 @@ export class LockComponent implements OnInit { private async setKeyAndContinue(key: SymmetricCryptoKey) { await this.cryptoService.setKey(key); - this.doContinue(); + await this.doContinue(); } private async doContinue() { - this.vaultTimeoutService.biometricLocked = false; - this.vaultTimeoutService.everBeenUnlocked = true; - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + await this.stateService.setBiometricLocked(false); + await this.stateService.setEverBeenUnlocked(true); + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); this.messagingService.send('unlocked'); if (this.onSuccessfulSubmit != null) { - this.onSuccessfulSubmit(); + await this.onSuccessfulSubmit(); } else if (this.router != null) { this.router.navigate([this.successRoute]); } } + + private async load() { + this.pinSet = await this.vaultTimeoutService.isPinLockSet(); + this.pinLock = (this.pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || this.pinSet[1]; + this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && + (await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric) || !this.platformUtilsService.supportsSecureStorage()); + this.biometricText = await this.stateService.getBiometricText(); + this.email = await this.stateService.getEmail(); + const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); + this.hideInput = usesKeyConnector && !this.pinLock; + + // Users with key connector and without biometric or pin has no MP to unlock using + if (usesKeyConnector && !(this.biometricLock || this.pinLock)) { + await this.vaultTimeoutService.logOut(); + } + + const webVaultUrl = this.environmentService.getWebVaultUrl(); + const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl; + this.webVaultHostname = Utils.getHostname(vaultUrl); + } } diff --git a/angular/src/components/login.component.ts b/angular/src/components/login.component.ts index 34c0b8fd00..5dc15c71dd 100644 --- a/angular/src/components/login.component.ts +++ b/angular/src/components/login.component.ts @@ -19,19 +19,11 @@ import { LogService } from 'jslib-common/abstractions/log.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; import { Utils } from 'jslib-common/misc/utils'; import { CaptchaProtectedComponent } from './captchaProtected.component'; -const Keys = { - rememberedEmail: 'rememberedEmail', - rememberEmail: 'rememberEmail', -}; - @Directive() export class LoginComponent extends CaptchaProtectedComponent implements OnInit { @Input() email: string = ''; @@ -53,22 +45,19 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit platformUtilsService: PlatformUtilsService, i18nService: I18nService, protected stateService: StateService, environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService, - protected logService: LogService, protected ngZone: NgZone) { + protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, + protected ngZone: NgZone) { super(environmentService, i18nService, platformUtilsService); } async ngOnInit() { if (this.email == null || this.email === '') { - this.email = await this.storageService.get(Keys.rememberedEmail); + this.email = await this.stateService.getRememberedEmail(); if (this.email == null) { this.email = ''; } } - this.rememberEmail = await this.storageService.get(Keys.rememberEmail); - if (this.rememberEmail == null) { - this.rememberEmail = true; - } + this.rememberEmail = await this.stateService.getRememberedEmail() != null; if (Utils.isBrowser && !Utils.isNode) { this.focusInput(); } @@ -96,11 +85,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit try { this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken); const response = await this.formPromise; - await this.storageService.save(Keys.rememberEmail, this.rememberEmail); if (this.rememberEmail) { - await this.storageService.save(Keys.rememberedEmail, this.email); + await this.stateService.setRememberedEmail(this.email); } else { - await this.storageService.remove(Keys.rememberedEmail); + await this.stateService.setRememberedEmail(null); } if (this.handleCaptchaRequired(response)) { return; @@ -117,8 +105,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit this.router.navigate([this.forcePasswordResetRoute]); } } else { - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } @@ -158,8 +146,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); // Save sso params - await this.storageService.save(ConstantsService.ssoStateKey, state); - await this.storageService.save(ConstantsService.ssoCodeVerifierKey, ssoCodeVerifier); + await this.stateService.setSsoState(state); + await this.stateService.setSsoCodeVerifier(ssoCodeVerifier); // Build URI const webUrl = this.environmentService.getWebVaultUrl(); diff --git a/angular/src/components/premium.component.ts b/angular/src/components/premium.component.ts index 5b5b4c89db..13efaa3b53 100644 --- a/angular/src/components/premium.component.ts +++ b/angular/src/components/premium.component.ts @@ -4,7 +4,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; @Directive() export class PremiumComponent implements OnInit { @@ -13,10 +13,11 @@ export class PremiumComponent implements OnInit { refreshPromise: Promise; constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected apiService: ApiService, protected userService: UserService, private logService: LogService) { } + protected apiService: ApiService, private logService: LogService, + protected stateService: StateService) { } async ngOnInit() { - this.isPremium = await this.userService.canAccessPremium(); + this.isPremium = await this.stateService.getCanAccessPremium(); } async refresh() { @@ -24,7 +25,7 @@ export class PremiumComponent implements OnInit { this.refreshPromise = this.apiService.refreshIdentityToken(); await this.refreshPromise; this.platformUtilsService.showToast('success', null, this.i18nService.t('refreshComplete')); - this.isPremium = await this.userService.canAccessPremium(); + this.isPremium = await this.stateService.getCanAccessPremium(); } catch (e) { this.logService.error(e); } diff --git a/angular/src/components/register.component.ts b/angular/src/components/register.component.ts index fd91af29c7..df0579bcfa 100644 --- a/angular/src/components/register.component.ts +++ b/angular/src/components/register.component.ts @@ -141,7 +141,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn const request = new RegisterRequest(this.email, this.name, hashedPassword, this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData, this.captchaToken); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); - const orgInvite = await this.stateService.get('orgInvitation'); + const orgInvite = await this.stateService.getOrganizationInvitation(); if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { request.token = orgInvite.token; request.organizationUserId = orgInvite.organizationUserId; diff --git a/angular/src/components/remove-password.component.ts b/angular/src/components/remove-password.component.ts index 91c35312b0..56132cbd22 100644 --- a/angular/src/components/remove-password.component.ts +++ b/angular/src/components/remove-password.component.ts @@ -8,11 +8,8 @@ import { ApiService } from 'jslib-common/abstractions/api.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; import { Organization } from 'jslib-common/models/domain/organization'; @@ -27,14 +24,14 @@ export class RemovePasswordComponent implements OnInit { organization: Organization; email: string; - constructor(private router: Router, private userService: UserService, + constructor(private router: Router, private stateService: StateService, private apiService: ApiService, private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private keyConnectorService: KeyConnectorService, private storageService: StorageService) { } + private keyConnectorService: KeyConnectorService) { } async ngOnInit() { this.organization = await this.keyConnectorService.getManagingOrganization(); - this.email = await this.userService.getEmail(); + this.email = await this.stateService.getEmail(); await this.syncService.fullSync(false); this.loading = false; } diff --git a/angular/src/components/send/add-edit.component.ts b/angular/src/components/send/add-edit.component.ts index 70bd87ba69..69d5c7d43a 100644 --- a/angular/src/components/send/add-edit.component.ts +++ b/angular/src/components/send/add-edit.component.ts @@ -17,7 +17,7 @@ import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { SendService } from 'jslib-common/abstractions/send.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { SendFileView } from 'jslib-common/models/view/sendFileView'; import { SendTextView } from 'jslib-common/models/view/sendTextView'; @@ -57,9 +57,9 @@ export class AddEditComponent implements OnInit { constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected datePipe: DatePipe, - protected sendService: SendService, protected userService: UserService, - protected messagingService: MessagingService, protected policyService: PolicyService, - private logService: LogService) { + protected sendService: SendService, protected messagingService: MessagingService, + protected policyService: PolicyService, private logService: LogService, + protected stateService: StateService) { this.typeOptions = [ { name: i18nService.t('sendTypeFile'), value: SendType.File }, { name: i18nService.t('sendTypeText'), value: SendType.Text }, @@ -108,8 +108,8 @@ export class AddEditComponent implements OnInit { this.disableHideEmail = await this.policyService.policyAppliesToUser(PolicyType.SendOptions, p => p.data.disableHideEmail); - this.canAccessPremium = await this.userService.canAccessPremium(); - this.emailVerified = await this.userService.getEmailVerified(); + this.canAccessPremium = await this.stateService.getCanAccessPremium(); + this.emailVerified = await this.stateService.getEmailVerified(); if (!this.canAccessPremium || !this.emailVerified) { this.type = SendType.Text; } diff --git a/angular/src/components/send/send.component.ts b/angular/src/components/send/send.component.ts index 77f36459a4..c9b83a5cd9 100644 --- a/angular/src/components/send/send.component.ts +++ b/angular/src/components/send/send.component.ts @@ -16,7 +16,6 @@ import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.se import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { SearchService } from 'jslib-common/abstractions/search.service'; import { SendService } from 'jslib-common/abstractions/send.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; @Directive() export class SendComponent implements OnInit { @@ -48,8 +47,7 @@ export class SendComponent implements OnInit { constructor(protected sendService: SendService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected ngZone: NgZone, protected searchService: SearchService, - protected policyService: PolicyService, protected userService: UserService, - private logService: LogService) { } + protected policyService: PolicyService, private logService: LogService) { } async ngOnInit() { this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend); diff --git a/angular/src/components/set-password.component.ts b/angular/src/components/set-password.component.ts index c12e32e6d3..c828a79347 100644 --- a/angular/src/components/set-password.component.ts +++ b/angular/src/components/set-password.component.ts @@ -13,8 +13,8 @@ import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { EncString } from 'jslib-common/models/domain/encString'; import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; @@ -42,12 +42,13 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { onSuccessfulChangePassword: () => Promise; successRoute = 'vault'; - constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, - userService: UserService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, policyService: PolicyService, protected router: Router, - private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute) { - super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, policyService); + constructor(i18nService: I18nService, cryptoService: CryptoService, + messagingService: MessagingService, passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, policyService: PolicyService, + protected router: Router, private apiService: ApiService, + private syncService: SyncService, private route: ActivatedRoute, stateService: StateService) { + super(i18nService, cryptoService, messagingService, passwordGenerationService, + platformUtilsService, policyService, stateService); } async ngOnInit() { @@ -102,7 +103,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { if (response == null) { throw new Error(this.i18nService.t('resetPasswordOrgKeysError')); } - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); const publicKey = Utils.fromB64ToArray(response.publicKey); // RSA Encrypt user's encKey.key with organization public key @@ -138,8 +139,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { } private async onSetPasswordSuccess(key: SymmetricCryptoKey, encKey: [SymmetricCryptoKey, EncString], keys: [string, EncString]) { - await this.userService.setInformation(await this.userService.getUserId(), await this.userService.getEmail(), - this.kdf, this.kdfIterations); + await this.stateService.setKdfType(this.kdf); + await this.stateService.setKdfIterations(this.kdfIterations); await this.cryptoService.setKey(key); await this.cryptoService.setEncKey(encKey[1].encryptedString); await this.cryptoService.setEncPrivateKey(keys[1].encryptedString); diff --git a/angular/src/components/set-pin.component.ts b/angular/src/components/set-pin.component.ts index d2cfae3a03..fc26d127a3 100644 --- a/angular/src/components/set-pin.component.ts +++ b/angular/src/components/set-pin.component.ts @@ -5,11 +5,7 @@ import { import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { Utils } from 'jslib-common/misc/utils'; @@ -17,15 +13,13 @@ import { ModalRef } from './modal/modal.ref'; @Directive() export class SetPinComponent implements OnInit { - pin = ''; showPin = false; masterPassOnRestart = true; showMasterPassOnRestart = true; - constructor(private modalRef: ModalRef, private cryptoService: CryptoService, private userService: UserService, - private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, - private keyConnectorService: KeyConnectorService) { } + constructor(private modalRef: ModalRef, private cryptoService: CryptoService, + private keyConnectorService: KeyConnectorService, private stateService: StateService) { } async ngOnInit() { this.showMasterPassOnRestart = this.masterPassOnRestart = !await this.keyConnectorService.getUsesKeyConnector(); @@ -40,18 +34,18 @@ export class SetPinComponent implements OnInit { this.modalRef.close(false); } - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - const email = await this.userService.getEmail(); + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); + const email = await this.stateService.getEmail(); const pinKey = await this.cryptoService.makePinKey(this.pin, email, kdf, kdfIterations); const key = await this.cryptoService.getKey(); const pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); if (this.masterPassOnRestart) { const encPin = await this.cryptoService.encrypt(this.pin); - await this.storageService.save(ConstantsService.protectedPin, encPin.encryptedString); - this.vaultTimeoutService.pinProtectedKey = pinProtectedKey; + await this.stateService.setProtectedPin(encPin.encryptedString); + await this.stateService.setDecryptedPinProtected(pinProtectedKey); } else { - await this.storageService.save(ConstantsService.pinProtectedKey, pinProtectedKey.encryptedString); + await this.stateService.setEncryptedPinProtected(pinProtectedKey.encryptedString); } this.modalRef.close(true); diff --git a/angular/src/components/share.component.ts b/angular/src/components/share.component.ts index e5badc3790..1812f427a5 100644 --- a/angular/src/components/share.component.ts +++ b/angular/src/components/share.component.ts @@ -12,8 +12,8 @@ import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; +import { OrganizationService } from 'jslib-common/abstractions/organization.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { Organization } from 'jslib-common/models/domain/organization'; import { CipherView } from 'jslib-common/models/view/cipherView'; @@ -35,8 +35,8 @@ export class ShareComponent implements OnInit { protected writeableCollections: CollectionView[] = []; constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, protected userService: UserService, - protected cipherService: CipherService, private logService: LogService) { } + protected i18nService: I18nService, protected cipherService: CipherService, + private logService: LogService, protected organizationService: OrganizationService) { } async ngOnInit() { await this.load(); @@ -45,7 +45,7 @@ export class ShareComponent implements OnInit { async load() { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map(c => c).filter(c => !c.readOnly); - const orgs = await this.userService.getAllOrganizations(); + const orgs = await this.organizationService.getAll(); this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')) .filter(o => o.enabled && o.status === OrganizationUserStatusType.Confirmed); diff --git a/angular/src/components/sso.component.ts b/angular/src/components/sso.component.ts index 1ab8e2f404..d08559fc1d 100644 --- a/angular/src/components/sso.component.ts +++ b/angular/src/components/sso.component.ts @@ -15,9 +15,6 @@ import { LogService } from 'jslib-common/abstractions/log.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; import { Utils } from 'jslib-common/misc/utils'; @@ -47,18 +44,18 @@ export class SsoComponent { constructor(protected authService: AuthService, protected router: Router, protected i18nService: I18nService, protected route: ActivatedRoute, - protected storageService: StorageService, protected stateService: StateService, - protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected cryptoFunctionService: CryptoFunctionService, protected environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationService, protected logService: LogService) { } + protected stateService: StateService, protected platformUtilsService: PlatformUtilsService, + protected apiService: ApiService, protected cryptoFunctionService: CryptoFunctionService, + protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, + protected logService: LogService) { } async ngOnInit() { this.route.queryParams.pipe(first()).subscribe(async qParams => { if (qParams.code != null && qParams.state != null) { - const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); - const state = await this.storageService.get(ConstantsService.ssoStateKey); - await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); - await this.storageService.remove(ConstantsService.ssoStateKey); + const codeVerifier = await this.stateService.getSsoCodeVerifier(); + const state = await this.stateService.getSsoState(); + await this.stateService.setSsoCodeVerifier(null); + await this.stateService.setSsoState(null); if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) { await this.logIn(qParams.code, codeVerifier, this.getOrgIdentifierFromState(qParams.state)); } @@ -106,7 +103,7 @@ export class SsoComponent { const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); + await this.stateService.setSsoCodeVerifier(codeVerifier); } if (state == null) { @@ -120,7 +117,7 @@ export class SsoComponent { state += `_identifier=${this.identifier}`; // Save state (regardless of new or existing) - await this.storageService.save(ConstantsService.ssoStateKey, state); + await this.stateService.setSsoState(state); let authorizeUrl = this.environmentService.getIdentityUrl() + '/connect/authorize?' + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + @@ -170,8 +167,8 @@ export class SsoComponent { this.router.navigate([this.forcePasswordResetRoute]); } } else { - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts index 5ad45a2525..02f5923d8b 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -20,10 +20,8 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; import { TwoFactorProviders } from 'jslib-common/services/auth.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; import * as DuoWebSDK from 'duo_web_sdk'; import { WebAuthnIFrame } from 'jslib-common/misc/webauthn_iframe'; @@ -58,8 +56,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected win: Window, protected environmentService: EnvironmentService, protected stateService: StateService, - protected storageService: StorageService, protected route: ActivatedRoute, - protected logService: LogService) { + protected route: ActivatedRoute, protected logService: LogService) { this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); } @@ -179,8 +176,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { async doSubmit() { this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); const response: AuthResult = await this.formPromise; - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } diff --git a/angular/src/components/update-temp-password.component.ts b/angular/src/components/update-temp-password.component.ts index 29e545219d..ffb27db279 100644 --- a/angular/src/components/update-temp-password.component.ts +++ b/angular/src/components/update-temp-password.component.ts @@ -8,8 +8,8 @@ import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component'; @@ -30,11 +30,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, passwordGenerationService: PasswordGenerationService, policyService: PolicyService, - cryptoService: CryptoService, userService: UserService, - messagingService: MessagingService, private apiService: ApiService, + cryptoService: CryptoService, messagingService: MessagingService, + private apiService: ApiService, stateService: StateService, private syncService: SyncService, private logService: LogService) { - super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, policyService); + super(i18nService, cryptoService, messagingService, passwordGenerationService, + platformUtilsService, policyService, stateService); } async ngOnInit() { @@ -49,9 +49,9 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); - this.email = await this.userService.getEmail(); - this.kdf = await this.userService.getKdf(); - this.kdfIterations = await this.userService.getKdfIterations(); + this.email = await this.stateService.getEmail(); + this.kdf = await this.stateService.getKdfType(); + this.kdfIterations = await this.stateService.getKdfIterations(); return true; } diff --git a/angular/src/components/view.component.ts b/angular/src/components/view.component.ts index 8e12634f3c..6a3da19ee6 100644 --- a/angular/src/components/view.component.ts +++ b/angular/src/components/view.component.ts @@ -24,9 +24,9 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { TokenService } from 'jslib-common/abstractions/token.service'; import { TotpService } from 'jslib-common/abstractions/totp.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; @@ -67,9 +67,9 @@ export class ViewComponent implements OnDestroy, OnInit { protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected win: Window, protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService, - protected eventService: EventService, protected apiService: ApiService, - protected passwordRepromptService: PasswordRepromptService, private logService: LogService) { } + protected changeDetectorRef: ChangeDetectorRef, protected eventService: EventService, + protected apiService: ApiService, protected passwordRepromptService: PasswordRepromptService, + private logService: LogService, protected stateService: StateService) { } ngOnInit() { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { @@ -96,7 +96,7 @@ export class ViewComponent implements OnDestroy, OnInit { const cipher = await this.cipherService.get(this.cipherId); this.cipher = await cipher.decrypt(); - this.canAccessPremium = await this.userService.canAccessPremium(); + this.canAccessPremium = await this.stateService.getCanAccessPremium(); if (this.cipher.type === CipherType.Login && this.cipher.login.totp && (cipher.organizationUseTotp || this.canAccessPremium)) { diff --git a/angular/src/services/auth-guard.service.ts b/angular/src/services/auth-guard.service.ts index 5656157756..942e5b7336 100644 --- a/angular/src/services/auth-guard.service.ts +++ b/angular/src/services/auth-guard.service.ts @@ -8,16 +8,17 @@ import { import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService) { } + constructor(private vaultTimeoutService: VaultTimeoutService, private router: Router, + private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, + private stateService: StateService) { } async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { - const isAuthed = await this.userService.isAuthenticated(); + const isAuthed = await this.stateService.getIsAuthenticated(); if (!isAuthed) { this.messagingService.send('authBlocked'); return false; diff --git a/angular/src/services/jslib-services.module.ts b/angular/src/services/jslib-services.module.ts index 2040fb1093..a413eb4bed 100644 --- a/angular/src/services/jslib-services.module.ts +++ b/angular/src/services/jslib-services.module.ts @@ -19,16 +19,18 @@ import { FileUploadService } from 'jslib-common/services/fileUpload.service'; import { FolderService } from 'jslib-common/services/folder.service'; import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; import { NotificationsService } from 'jslib-common/services/notifications.service'; +import { OrganizationService } from 'jslib-common/services/organization.service'; import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; import { PolicyService } from 'jslib-common/services/policy.service'; +import { ProviderService } from 'jslib-common/services/provider.service'; import { SearchService } from 'jslib-common/services/search.service'; import { SendService } from 'jslib-common/services/send.service'; import { SettingsService } from 'jslib-common/services/settings.service'; import { StateService } from 'jslib-common/services/state.service'; +import { StateMigrationService } from 'jslib-common/services/stateMigration.service'; import { SyncService } from 'jslib-common/services/sync.service'; import { TokenService } from 'jslib-common/services/token.service'; import { TotpService } from 'jslib-common/services/totp.service'; -import { UserService } from 'jslib-common/services/user.service'; import { UserVerificationService } from 'jslib-common/services/userVerification.service'; import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service'; import { WebCryptoFunctionService } from 'jslib-common/services/webCryptoFunction.service'; @@ -42,7 +44,7 @@ import { CipherService as CipherServiceAbstraction } from 'jslib-common/abstract import { CollectionService as CollectionServiceAbstraction } from 'jslib-common/abstractions/collection.service'; import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service'; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService as EnvironmentServiceAbstraction, Urls } from 'jslib-common/abstractions/environment.service'; +import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service'; import { EventService as EventServiceAbstraction } from 'jslib-common/abstractions/event.service'; import { ExportService as ExportServiceAbstraction } from 'jslib-common/abstractions/export.service'; import { FileUploadService as FileUploadServiceAbstraction } from 'jslib-common/abstractions/fileUpload.service'; @@ -52,21 +54,23 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from 'jslib-com import { LogService } from 'jslib-common/abstractions/log.service'; import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service'; import { NotificationsService as NotificationsServiceAbstraction } from 'jslib-common/abstractions/notifications.service'; +import { OrganizationService as OrganizationServiceAbstraction } from 'jslib-common/abstractions/organization.service'; import { PasswordGenerationService as PasswordGenerationServiceAbstraction, } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service'; +import { ProviderService as ProviderServiceAbstraction } from 'jslib-common/abstractions/provider.service'; import { SearchService as SearchServiceAbstraction } from 'jslib-common/abstractions/search.service'; import { SendService as SendServiceAbstraction } from 'jslib-common/abstractions/send.service'; import { SettingsService as SettingsServiceAbstraction } from 'jslib-common/abstractions/settings.service'; import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service'; +import { StateMigrationService as StateMigrationServiceAbstraction } from 'jslib-common/abstractions/stateMigration.service'; import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; import { SyncService as SyncServiceAbstraction } from 'jslib-common/abstractions/sync.service'; import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service'; import { TotpService as TotpServiceAbstraction } from 'jslib-common/abstractions/totp.service'; -import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service'; import { UserVerificationService as UserVerificationServiceAbstraction } from 'jslib-common/abstractions/userVerification.service'; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib-common/abstractions/vaultTimeout.service'; @@ -108,7 +112,6 @@ import { ValidationService } from './validation.service'; deps: [ CryptoServiceAbstraction, ApiServiceAbstraction, - UserServiceAbstraction, TokenServiceAbstraction, AppIdServiceAbstraction, I18nServiceAbstraction, @@ -117,28 +120,41 @@ import { ValidationService } from './validation.service'; VaultTimeoutServiceAbstraction, LogService, CryptoFunctionServiceAbstraction, - EnvironmentServiceAbstraction, KeyConnectorServiceAbstraction, + EnvironmentServiceAbstraction, + StateServiceAbstraction, ], }, { provide: CipherServiceAbstraction, - useFactory: (cryptoService: CryptoServiceAbstraction, userService: UserServiceAbstraction, - settingsService: SettingsServiceAbstraction, apiService: ApiServiceAbstraction, - fileUploadService: FileUploadServiceAbstraction, storageService: StorageServiceAbstraction, - i18nService: I18nServiceAbstraction, injector: Injector, logService: LogService) => - new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => injector.get(SearchServiceAbstraction), logService), + useFactory: ( + cryptoService: CryptoServiceAbstraction, + settingsService: SettingsServiceAbstraction, + apiService: ApiServiceAbstraction, + fileUploadService: FileUploadServiceAbstraction, + i18nService: I18nServiceAbstraction, + injector: Injector, + logService: LogService, + stateService: StateServiceAbstraction, + ) => new CipherService( + cryptoService, + settingsService, + apiService, + fileUploadService, + i18nService, + () => injector.get(SearchServiceAbstraction), + logService, + stateService, + ), deps: [ CryptoServiceAbstraction, - UserServiceAbstraction, SettingsServiceAbstraction, ApiServiceAbstraction, FileUploadServiceAbstraction, - StorageServiceAbstraction, I18nServiceAbstraction, Injector, // TODO: Get rid of this circular dependency! LogService, + StateServiceAbstraction, ], }, { @@ -146,11 +162,10 @@ import { ValidationService } from './validation.service'; useClass: FolderService, deps: [ CryptoServiceAbstraction, - UserServiceAbstraction, ApiServiceAbstraction, - StorageServiceAbstraction, I18nServiceAbstraction, CipherServiceAbstraction, + StateServiceAbstraction, ], }, { provide: LogService, useFactory: () => new ConsoleLogService(false) }, @@ -159,35 +174,33 @@ import { ValidationService } from './validation.service'; useClass: CollectionService, deps: [ CryptoServiceAbstraction, - UserServiceAbstraction, - StorageServiceAbstraction, I18nServiceAbstraction, + StateServiceAbstraction, ], }, { provide: EnvironmentServiceAbstraction, useClass: EnvironmentService, - deps: [StorageServiceAbstraction], + deps: [StateServiceAbstraction], }, { provide: TotpServiceAbstraction, useClass: TotpService, deps: [ - StorageServiceAbstraction, CryptoFunctionServiceAbstraction, LogService, + StateServiceAbstraction, ], }, - { provide: TokenServiceAbstraction, useClass: TokenService, deps: [StorageServiceAbstraction] }, + { provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }, { provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ - StorageServiceAbstraction, - 'SECURE_STORAGE', CryptoFunctionServiceAbstraction, PlatformUtilsServiceAbstraction, LogService, + StateServiceAbstraction, ], }, { @@ -195,8 +208,8 @@ import { ValidationService } from './validation.service'; useClass: PasswordGenerationService, deps: [ CryptoServiceAbstraction, - StorageServiceAbstraction, PolicyServiceAbstraction, + StateServiceAbstraction, ], }, { @@ -222,71 +235,121 @@ import { ValidationService } from './validation.service'; }, { provide: SyncServiceAbstraction, - useFactory: (userService: UserServiceAbstraction, apiService: ApiServiceAbstraction, - settingsService: SettingsServiceAbstraction, folderService: FolderServiceAbstraction, - cipherService: CipherServiceAbstraction, cryptoService: CryptoServiceAbstraction, - collectionService: CollectionServiceAbstraction, storageService: StorageServiceAbstraction, - messagingService: MessagingServiceAbstraction, policyService: PolicyServiceAbstraction, - sendService: SendServiceAbstraction, logService: LogService, tokenService: TokenService, - keyConnectorService: KeyConnectorServiceAbstraction) => new SyncService(userService, apiService, - settingsService, folderService, cipherService, cryptoService, collectionService, storageService, - messagingService, policyService, sendService, logService, tokenService, keyConnectorService, - async (expired: boolean) => messagingService.send('logout', { expired: expired })), + useFactory: ( + apiService: ApiServiceAbstraction, + settingsService: SettingsServiceAbstraction, + folderService: FolderServiceAbstraction, + cipherService: CipherServiceAbstraction, + cryptoService: CryptoServiceAbstraction, + collectionService: CollectionServiceAbstraction, + messagingService: MessagingServiceAbstraction, + policyService: PolicyServiceAbstraction, + sendService: SendServiceAbstraction, + logService: LogService, + keyConnectorService: KeyConnectorServiceAbstraction, + stateService: StateServiceAbstraction, + organizationService: OrganizationServiceAbstraction, + providerService: ProviderServiceAbstraction, + ) => new SyncService( + apiService, + settingsService, + folderService, + cipherService, + cryptoService, + collectionService, + messagingService, + policyService, + sendService, + logService, + keyConnectorService, + stateService, + organizationService, + providerService, + async (expired: boolean) => messagingService.send('logout', { expired: expired })), deps: [ - UserServiceAbstraction, ApiServiceAbstraction, SettingsServiceAbstraction, FolderServiceAbstraction, CipherServiceAbstraction, CryptoServiceAbstraction, CollectionServiceAbstraction, - StorageServiceAbstraction, MessagingServiceAbstraction, PolicyServiceAbstraction, SendServiceAbstraction, LogService, - TokenServiceAbstraction, KeyConnectorServiceAbstraction, + StateServiceAbstraction, + OrganizationServiceAbstraction, + ProviderServiceAbstraction, ], }, - { - provide: UserServiceAbstraction, - useClass: UserService, - deps: [TokenServiceAbstraction, StorageServiceAbstraction], - }, { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, { provide: SettingsServiceAbstraction, useClass: SettingsService, - deps: [UserServiceAbstraction, StorageServiceAbstraction], + deps: [StateServiceAbstraction], }, { provide: VaultTimeoutServiceAbstraction, - useFactory: (cipherService: CipherServiceAbstraction, folderService: FolderServiceAbstraction, - collectionService: CollectionServiceAbstraction, cryptoService: CryptoServiceAbstraction, - platformUtilsService: PlatformUtilsServiceAbstraction, storageService: StorageServiceAbstraction, - messagingService: MessagingServiceAbstraction, searchService: SearchServiceAbstraction, - userService: UserServiceAbstraction, tokenService: TokenServiceAbstraction, - policyService: PolicyServiceAbstraction, keyConnectorService: KeyConnectorServiceAbstraction) => - new VaultTimeoutService(cipherService, folderService, collectionService, cryptoService, - platformUtilsService, storageService, messagingService, searchService, userService, tokenService, - policyService, keyConnectorService, null, - async () => messagingService.send('logout', { expired: false })), + useFactory: ( + cipherService: CipherServiceAbstraction, + folderService: FolderServiceAbstraction, + collectionService: CollectionServiceAbstraction, + cryptoService: CryptoServiceAbstraction, + platformUtilsService: PlatformUtilsServiceAbstraction, + messagingService: MessagingServiceAbstraction, + searchService: SearchServiceAbstraction, + tokenService: TokenServiceAbstraction, + policyService: PolicyServiceAbstraction, + keyConnectorService: KeyConnectorServiceAbstraction, + stateService: StateServiceAbstraction, + ) => new VaultTimeoutService( + cipherService, + folderService, + collectionService, + cryptoService, + platformUtilsService, + messagingService, + searchService, + tokenService, + policyService, + keyConnectorService, + stateService, + null, + async () => messagingService.send('logout', { expired: false }), + ), deps: [ CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, CryptoServiceAbstraction, PlatformUtilsServiceAbstraction, - StorageServiceAbstraction, MessagingServiceAbstraction, SearchServiceAbstraction, - UserServiceAbstraction, TokenServiceAbstraction, PolicyServiceAbstraction, + KeyConnectorServiceAbstraction, + StateServiceAbstraction, + ], + }, + { + provide: StateServiceAbstraction, + useClass: StateService, + deps: [ + StorageServiceAbstraction, + 'SECURE_STORAGE', + LogService, + StateMigrationServiceAbstraction, + ], + }, + { + provide: StateMigrationServiceAbstraction, + useClass: StateMigrationService, + deps: [ + StorageServiceAbstraction, + 'SECURE_STORAGE', ], }, - { provide: StateServiceAbstraction, useClass: StateService }, { provide: ExportServiceAbstraction, useClass: ExportService, @@ -308,14 +371,26 @@ import { ValidationService } from './validation.service'; }, { provide: NotificationsServiceAbstraction, - useFactory: (userService: UserServiceAbstraction, syncService: SyncServiceAbstraction, - appIdService: AppIdServiceAbstraction, apiService: ApiServiceAbstraction, - vaultTimeoutService: VaultTimeoutServiceAbstraction, environmentService: EnvironmentServiceAbstraction, - messagingService: MessagingServiceAbstraction, logService: LogService) => - new NotificationsService(userService, syncService, appIdService, apiService, vaultTimeoutService, - environmentService, async () => messagingService.send('logout', { expired: true }), logService), + useFactory: ( + syncService: SyncServiceAbstraction, + appIdService: AppIdServiceAbstraction, + apiService: ApiServiceAbstraction, + vaultTimeoutService: VaultTimeoutServiceAbstraction, + environmentService: EnvironmentServiceAbstraction, + messagingService: MessagingServiceAbstraction, + logService: LogService, + stateService: StateServiceAbstraction, + ) => new NotificationsService( + syncService, + appIdService, + apiService, + vaultTimeoutService, + environmentService, + async () => messagingService.send('logout', { expired: true }), + logService, + stateService, + ), deps: [ - UserServiceAbstraction, SyncServiceAbstraction, AppIdServiceAbstraction, ApiServiceAbstraction, @@ -323,6 +398,7 @@ import { ValidationService } from './validation.service'; EnvironmentServiceAbstraction, MessagingServiceAbstraction, LogService, + StateServiceAbstraction, ], }, { @@ -334,19 +410,19 @@ import { ValidationService } from './validation.service'; provide: EventServiceAbstraction, useClass: EventService, deps: [ - StorageServiceAbstraction, ApiServiceAbstraction, - UserServiceAbstraction, CipherServiceAbstraction, + StateServiceAbstraction, LogService, + OrganizationServiceAbstraction, ], }, { provide: PolicyServiceAbstraction, useClass: PolicyService, deps: [ - UserServiceAbstraction, - StorageServiceAbstraction, + StateServiceAbstraction, + OrganizationServiceAbstraction, ApiServiceAbstraction, ], }, @@ -355,24 +431,23 @@ import { ValidationService } from './validation.service'; useClass: SendService, deps: [ CryptoServiceAbstraction, - UserServiceAbstraction, ApiServiceAbstraction, FileUploadServiceAbstraction, - StorageServiceAbstraction, I18nServiceAbstraction, CryptoFunctionServiceAbstraction, + StateServiceAbstraction, ], }, { provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ - StorageServiceAbstraction, - UserServiceAbstraction, + StateServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, LogService, + OrganizationServiceAbstraction, ], }, { @@ -385,6 +460,20 @@ import { ValidationService } from './validation.service'; ], }, { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, + { + provide: OrganizationServiceAbstraction, + useClass: OrganizationService, + deps: [ + StateServiceAbstraction, + ], + }, + { + provide: ProviderServiceAbstraction, + useClass: ProviderService, + deps: [ + StateServiceAbstraction, + ], + }, ], }) export class JslibServicesModule { diff --git a/angular/src/services/lock-guard.service.ts b/angular/src/services/lock-guard.service.ts index 400eedc555..bde728e302 100644 --- a/angular/src/services/lock-guard.service.ts +++ b/angular/src/services/lock-guard.service.ts @@ -4,29 +4,26 @@ import { Router, } from '@angular/router'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; @Injectable() export class LockGuardService implements CanActivate { - protected homepage = 'vault'; - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router) { } + constructor(private vaultTimeoutService: VaultTimeoutService, private router: Router, + private stateService: StateService) { } async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); - if (isAuthed) { - const locked = await this.vaultTimeoutService.isLocked(); - if (locked) { - return true; - } else { - this.router.navigate([this.homepage]); - return false; - } + if (!await this.stateService.getIsAuthenticated()) { + this.router.navigate(['login']); + return false; } - this.router.navigate(['']); - return false; + if (!await this.vaultTimeoutService.isLocked()) { + this.router.navigate([this.homepage]); + return false; + } + + return true; } } diff --git a/angular/src/services/unauth-guard.service.ts b/angular/src/services/unauth-guard.service.ts index 786241b887..26b0d53cdb 100644 --- a/angular/src/services/unauth-guard.service.ts +++ b/angular/src/services/unauth-guard.service.ts @@ -1,22 +1,21 @@ import { Injectable } from '@angular/core'; import { - ActivatedRouteSnapshot, CanActivate, Router, } from '@angular/router'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; @Injectable() export class UnauthGuardService implements CanActivate { protected homepage = 'vault'; - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router) { } + constructor(private vaultTimeoutService: VaultTimeoutService, private router: Router, + private stateService: StateService) { } async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); + const isAuthed = await this.stateService.getIsAuthenticated(); if (isAuthed) { const locked = await this.vaultTimeoutService.isLocked(); if (locked) { @@ -26,7 +25,6 @@ export class UnauthGuardService implements CanActivate { } return false; } - return true; } } diff --git a/common/src/abstractions/apiKey.service.ts b/common/src/abstractions/apiKey.service.ts deleted file mode 100644 index bc4b8a4437..0000000000 --- a/common/src/abstractions/apiKey.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -export abstract class ApiKeyService { - setInformation: (clientId: string, clientSecret: string) => Promise; - clear: () => Promise; - getClientId: () => Promise; - getClientSecret: () => Promise; - getEntityType: () => Promise; - getEntityId: () => Promise; - isAuthenticated: () => Promise; -} diff --git a/common/src/abstractions/cipher.service.ts b/common/src/abstractions/cipher.service.ts index 941c3826ac..d8db4a628c 100644 --- a/common/src/abstractions/cipher.service.ts +++ b/common/src/abstractions/cipher.service.ts @@ -11,9 +11,7 @@ import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; export abstract class CipherService { - decryptedCipherCache: CipherView[]; - - clearCache: () => void; + clearCache: (userId?: string) => Promise; encrypt: (model: CipherView, key?: SymmetricCryptoKey, originalCipher?: Cipher) => Promise; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; diff --git a/common/src/abstractions/collection.service.ts b/common/src/abstractions/collection.service.ts index 0904553793..987fcdd049 100644 --- a/common/src/abstractions/collection.service.ts +++ b/common/src/abstractions/collection.service.ts @@ -6,9 +6,7 @@ import { TreeNode } from '../models/domain/treeNode'; import { CollectionView } from '../models/view/collectionView'; export abstract class CollectionService { - decryptedCollectionCache: CollectionView[]; - - clearCache: () => void; + clearCache: (userId?: string) => Promise; encrypt: (model: CollectionView) => Promise; decryptMany: (collections: Collection[]) => Promise; get: (id: string) => Promise; diff --git a/common/src/abstractions/crypto.service.ts b/common/src/abstractions/crypto.service.ts index 841e61ab07..34d38108dd 100644 --- a/common/src/abstractions/crypto.service.ts +++ b/common/src/abstractions/crypto.service.ts @@ -8,17 +8,16 @@ import { ProfileProviderResponse } from '../models/response/profileProviderRespo import { HashPurpose } from '../enums/hashPurpose'; import { KdfType } from '../enums/kdfType'; - -import { KeySuffixOptions } from './storage.service'; +import { KeySuffixOptions } from '../enums/keySuffixOptions'; export abstract class CryptoService { setKey: (key: SymmetricCryptoKey) => Promise; - setKeyHash: (keyHash: string) => Promise<{}>; - setEncKey: (encKey: string) => Promise<{}>; - setEncPrivateKey: (encPrivateKey: string) => Promise<{}>; - setOrgKeys: (orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]) => Promise<{}>; - setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<{}>; - getKey: (keySuffix?: KeySuffixOptions) => Promise; + setKeyHash: (keyHash: string) => Promise; + setEncKey: (encKey: string) => Promise; + setEncPrivateKey: (encPrivateKey: string) => Promise; + setOrgKeys: (orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]) => Promise; + setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise; + getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; getKeyFromStorage: (keySuffix: KeySuffixOptions) => Promise; getKeyHash: () => Promise; compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise; @@ -30,17 +29,17 @@ export abstract class CryptoService { getOrgKey: (orgId: string) => Promise; getProviderKey: (providerId: string) => Promise; hasKey: () => Promise; - hasKeyInMemory: () => boolean; - hasKeyStored: (keySuffix?: KeySuffixOptions) => Promise; + hasKeyInMemory: (userId?: string) => Promise; + hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; hasEncKey: () => Promise; - clearKey: (clearSecretStorage?: boolean) => Promise; + clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise; clearKeyHash: () => Promise; - clearEncKey: (memoryOnly?: boolean) => Promise; - clearKeyPair: (memoryOnly?: boolean) => Promise; - clearOrgKeys: (memoryOnly?: boolean) => Promise; + clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise; + clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise; + clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise; clearProviderKeys: (memoryOnly?: boolean) => Promise; clearPinProtectedKey: () => Promise; - clearKeys: () => Promise; + clearKeys: (userId?: string) => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeKeyFromPin: (pin: string, salt: string, kdf: KdfType, kdfIterations: number, diff --git a/common/src/abstractions/event.service.ts b/common/src/abstractions/event.service.ts index 40c080275f..ace1027817 100644 --- a/common/src/abstractions/event.service.ts +++ b/common/src/abstractions/event.service.ts @@ -2,6 +2,6 @@ import { EventType } from '../enums/eventType'; export abstract class EventService { collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise; - uploadEvents: () => Promise; - clearEvents: () => Promise; + uploadEvents: (userId?: string) => Promise; + clearEvents: (userId?: string) => Promise; } diff --git a/common/src/abstractions/folder.service.ts b/common/src/abstractions/folder.service.ts index f1997ffef5..339aec892d 100644 --- a/common/src/abstractions/folder.service.ts +++ b/common/src/abstractions/folder.service.ts @@ -7,9 +7,7 @@ import { TreeNode } from '../models/domain/treeNode'; import { FolderView } from '../models/view/folderView'; export abstract class FolderService { - decryptedFolderCache: FolderView[]; - - clearCache: () => void; + clearCache: (userId?: string) => Promise; encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; getAll: () => Promise; diff --git a/common/src/abstractions/organization.service.ts b/common/src/abstractions/organization.service.ts new file mode 100644 index 0000000000..a9af9faaac --- /dev/null +++ b/common/src/abstractions/organization.service.ts @@ -0,0 +1,11 @@ +import { OrganizationData } from '../models/data/organizationData'; + +import { Organization } from '../models/domain/organization'; + +export abstract class OrganizationService { + get: (id: string) => Promise; + getByIdentifier: (identifier: string) => Promise; + getAll: (userId?: string) => Promise; + save: (orgs: {[id: string]: OrganizationData}) => Promise; + canManageSponsorships: () => Promise; +} diff --git a/common/src/abstractions/passwordGeneration.service.ts b/common/src/abstractions/passwordGeneration.service.ts index 52d18dde7c..163e09c46e 100644 --- a/common/src/abstractions/passwordGeneration.service.ts +++ b/common/src/abstractions/passwordGeneration.service.ts @@ -12,7 +12,7 @@ export abstract class PasswordGenerationService { saveOptions: (options: any) => Promise; getHistory: () => Promise; addHistory: (password: string) => Promise; - clear: () => Promise; + clear: (userId?: string) => Promise; passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void; } diff --git a/common/src/abstractions/policy.service.ts b/common/src/abstractions/policy.service.ts index 6acc8253fc..84ff94ac5b 100644 --- a/common/src/abstractions/policy.service.ts +++ b/common/src/abstractions/policy.service.ts @@ -10,17 +10,15 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { PolicyType } from '../enums/policyType'; export abstract class PolicyService { - policyCache: Policy[]; - clearCache: () => void; - getAll: (type?: PolicyType) => Promise; + getAll: (type?: PolicyType, userId?: string) => Promise; getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise; replace: (policies: { [id: string]: PolicyData; }) => Promise; - clear: (userId: string) => Promise; + clear: (userId?: string) => Promise; getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise; evaluateMasterPassword: (passwordStrength: number, newPassword: string, enforcedPolicyOptions?: MasterPasswordPolicyOptions) => boolean; getResetPasswordPolicyOptions: (policies: Policy[], orgId: string) => [ResetPasswordPolicyOptions, boolean]; mapPoliciesFromToken: (policiesResponse: ListResponse) => Policy[]; - policyAppliesToUser: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Promise; + policyAppliesToUser: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean, userId?: string) => Promise; } diff --git a/common/src/abstractions/provider.service.ts b/common/src/abstractions/provider.service.ts new file mode 100644 index 0000000000..e3ca562569 --- /dev/null +++ b/common/src/abstractions/provider.service.ts @@ -0,0 +1,9 @@ +import { ProviderData } from '../models/data/providerData'; + +import { Provider } from '../models/domain/provider'; + +export abstract class ProviderService { + get: (id: string) => Promise; + getAll: () => Promise; + save: (providers: {[id: string]: ProviderData}) => Promise; +} diff --git a/common/src/abstractions/send.service.ts b/common/src/abstractions/send.service.ts index f7ede22376..2763dcda90 100644 --- a/common/src/abstractions/send.service.ts +++ b/common/src/abstractions/send.service.ts @@ -7,9 +7,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { SendView } from '../models/view/sendView'; export abstract class SendService { - decryptedSendCache: SendView[]; - - clearCache: () => void; + clearCache: () => Promise; encrypt: (model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey) => Promise<[Send, EncArrayBuffer]>; get: (id: string) => Promise; getAll: () => Promise; diff --git a/common/src/abstractions/settings.service.ts b/common/src/abstractions/settings.service.ts index 6104c1d1be..1112d557a1 100644 --- a/common/src/abstractions/settings.service.ts +++ b/common/src/abstractions/settings.service.ts @@ -1,6 +1,6 @@ export abstract class SettingsService { - clearCache: () => void; + clearCache: () => Promise; getEquivalentDomains: () => Promise; setEquivalentDomains: (equivalentDomains: string[][]) => Promise; - clear: (userId: string) => Promise; + clear: (userId?: string) => Promise; } diff --git a/common/src/abstractions/state.service.ts b/common/src/abstractions/state.service.ts index 78658882cd..042639fe8c 100644 --- a/common/src/abstractions/state.service.ts +++ b/common/src/abstractions/state.service.ts @@ -1,6 +1,259 @@ +import { BehaviorSubject } from 'rxjs'; + +import { KdfType } from '../enums/kdfType'; +import { UriMatchType } from '../enums/uriMatchType'; + +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { EventData } from '../models/data/eventData'; +import { FolderData } from '../models/data/folderData'; +import { OrganizationData } from '../models/data/organizationData'; +import { PolicyData } from '../models/data/policyData'; +import { ProviderData } from '../models/data/providerData'; +import { SendData } from '../models/data/sendData'; + +import { Account } from '../models/domain/account'; +import { EncString } from '../models/domain/encString'; +import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { Policy } from '../models/domain/policy'; +import { StorageOptions } from '../models/domain/storageOptions'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; +import { FolderView } from '../models/view/folderView'; +import { SendView } from '../models/view/sendView'; + export abstract class StateService { - get: (key: string) => Promise; - save: (key: string, obj: any) => Promise; - remove: (key: string) => Promise; - purge: () => Promise; + accounts: BehaviorSubject<{ [userId: string]: Account }>; + activeAccount: BehaviorSubject; + + addAccount: (account: Account) => Promise; + setActiveUser: (userId: string) => Promise; + clean: (options?: StorageOptions) => Promise; + init: () => Promise; + + getAccessToken: (options?: StorageOptions) => Promise; + setAccessToken: (value: string, options?: StorageOptions) => Promise; + getAddEditCipherInfo: (options?: StorageOptions) => Promise; + setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise; + getAlwaysShowDock: (options?: StorageOptions) => Promise; + setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; + getApiKeyClientId: (options?: StorageOptions) => Promise; + setApiKeyClientId: (value: string, options?: StorageOptions) => Promise; + getApiKeyClientSecret: (options?: StorageOptions) => Promise; + setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise; + getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; + setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; + getAutoFillOnPageLoadDefault: (options?: StorageOptions) => Promise; + setAutoFillOnPageLoadDefault: (value: boolean, options?: StorageOptions) => Promise; + getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise; + setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise; + getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; + setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; + getBiometricLocked: (options?: StorageOptions) => Promise; + setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise; + getBiometricText: (options?: StorageOptions) => Promise; + setBiometricText: (value: string, options?: StorageOptions) => Promise; + getBiometricUnlock: (options?: StorageOptions) => Promise; + setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise; + getCanAccessPremium: (options?: StorageOptions) => Promise; + getClearClipboard: (options?: StorageOptions) => Promise; + setClearClipboard: (value: number, options?: StorageOptions) => Promise; + getCollapsedGroupings: (options?: StorageOptions) => Promise>; + setCollapsedGroupings: (value: Set, options?: StorageOptions) => Promise; + getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; + setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise; + getCryptoMasterKey: (options?: StorageOptions) => Promise; + setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; + getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise; + setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise; + getCryptoMasterKeyB64: (options?: StorageOptions) => Promise; + setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise; + getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; + hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; + setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise; + getDecodedToken: (options?: StorageOptions) => Promise; + setDecodedToken: (value: any, options?: StorageOptions) => Promise; + getDecryptedCiphers: (options?: StorageOptions) => Promise; + setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; + getDecryptedCollections: (options?: StorageOptions) => Promise; + setDecryptedCollections: (value: CollectionView[], options?: StorageOptions) => Promise; + getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; + setDecryptedCryptoSymmetricKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; + getDecryptedFolders: (options?: StorageOptions) => Promise; + setDecryptedFolders: (value: FolderView[], options?: StorageOptions) => Promise; + getDecryptedOrganizationKeys: (options?: StorageOptions) => Promise>; + setDecryptedOrganizationKeys: (value: Map, options?: StorageOptions) => Promise; + getDecryptedPasswordGenerationHistory: (options?: StorageOptions) => Promise; + setDecryptedPasswordGenerationHistory: (value: GeneratedPasswordHistory[], options?: StorageOptions) => Promise; + getDecryptedPinProtected: (options?: StorageOptions) => Promise; + setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise; + getDecryptedPolicies: (options?: StorageOptions) => Promise; + setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise; + getDecryptedPrivateKey: (options?: StorageOptions) => Promise; + setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; + getDecryptedProviderKeys: (options?: StorageOptions) => Promise>; + setDecryptedProviderKeys: (value: Map, options?: StorageOptions) => Promise; + getDecryptedSends: (options?: StorageOptions) => Promise; + setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise; + getDefaultUriMatch: (options?: StorageOptions) => Promise; + setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise; + getDisableAddLoginNotification: (options?: StorageOptions) => Promise; + setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise; + getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise; + setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise; + getDisableAutoTotpCopy: (options?: StorageOptions) => Promise; + setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise; + getDisableBadgeCounter: (options?: StorageOptions) => Promise; + setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise; + getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise; + setDisableChangedPasswordNotification: (value: boolean, options?: StorageOptions) => Promise; + getDisableContextMenuItem: (options?: StorageOptions) => Promise; + setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise; + getDisableFavicon: (options?: StorageOptions) => Promise; + setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise; + getDisableGa: (options?: StorageOptions) => Promise; + setDisableGa: (value: boolean, options?: StorageOptions) => Promise; + getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise; + setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise; + getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise; + setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise; + getEmail: (options?: StorageOptions) => Promise; + setEmail: (value: string, options?: StorageOptions) => Promise; + getEmailVerified: (options?: StorageOptions) => Promise; + setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; + getEnableAlwaysOnTop: (options?: StorageOptions) => Promise; + setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise; + getEnableAutoFillOnPageLoad: (options?: StorageOptions) => Promise; + setEnableAutoFillOnPageLoad: (value: boolean, options?: StorageOptions) => Promise; + getEnableBiometric: (options?: StorageOptions) => Promise; + setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise; + getEnableBrowserIntegration: (options?: StorageOptions) => Promise; + setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; + getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; + setEnableBrowserIntegrationFingerprint: (value: boolean, options?: StorageOptions) => Promise; + getEnableCloseToTray: (options?: StorageOptions) => Promise; + setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableFullWidth: (options?: StorageOptions) => Promise; + setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise; + getEnableGravitars: (options?: StorageOptions) => Promise; + setEnableGravitars: (value: boolean, options?: StorageOptions) => Promise; + getEnableMinimizeToTray: (options?: StorageOptions) => Promise; + setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableStartToTray: (options?: StorageOptions) => Promise; + setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableTray: (options?: StorageOptions) => Promise; + setEnableTray: (value: boolean, options?: StorageOptions) => Promise; + getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; + setEncryptedCiphers: (value: { [id: string]: CipherData }, options?: StorageOptions) => Promise; + getEncryptedCollections: (options?: StorageOptions) => Promise<{ [id: string]: CollectionData }>; + setEncryptedCollections: (value: { [id: string]: CollectionData }, options?: StorageOptions) => Promise; + getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; + setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise; + getEncryptedFolders: (options?: StorageOptions) => Promise<{ [id: string]: FolderData }>; + setEncryptedFolders: (value: { [id: string]: FolderData }, options?: StorageOptions) => Promise; + getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise; + setEncryptedOrganizationKeys: (value: Map, options?: StorageOptions) => Promise; + getEncryptedPasswordGenerationHistory: (options?: StorageOptions) => Promise; + setEncryptedPasswordGenerationHistory: (value: GeneratedPasswordHistory[], options?: StorageOptions) => Promise; + getEncryptedPinProtected: (options?: StorageOptions) => Promise; + setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; + getEncryptedPolicies: (options?: StorageOptions) => Promise<{ [id: string]: PolicyData }>; + setEncryptedPolicies: (value: { [id: string]: PolicyData }, options?: StorageOptions) => Promise; + getEncryptedPrivateKey: (options?: StorageOptions) => Promise; + setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise; + getEncryptedProviderKeys: (options?: StorageOptions) => Promise; + setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise; + getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>; + setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise; + getEntityId: (options?: StorageOptions) => Promise; + setEntityId: (value: string, options?: StorageOptions) => Promise; + getEntityType: (options?: StorageOptions) => Promise; + setEntityType: (value: string, options?: StorageOptions) => Promise; + getEnvironmentUrls: (options?: StorageOptions) => Promise; + setEnvironmentUrls: (value: any, options?: StorageOptions) => Promise; + getEquivalentDomains: (options?: StorageOptions) => Promise; + setEquivalentDomains: (value: string, options?: StorageOptions) => Promise; + getEventCollection: (options?: StorageOptions) => Promise; + setEventCollection: (value: EventData[], options?: StorageOptions) => Promise; + getEverBeenUnlocked: (options?: StorageOptions) => Promise; + setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; + getForcePasswordReset: (options?: StorageOptions) => Promise; + setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise; + getInstalledVersion: (options?: StorageOptions) => Promise; + setInstalledVersion: (value: string, options?: StorageOptions) => Promise; + getIsAuthenticated: (options?: StorageOptions) => Promise; + getKdfIterations: (options?: StorageOptions) => Promise; + setKdfIterations: (value: number, options?: StorageOptions) => Promise; + getKdfType: (options?: StorageOptions) => Promise; + setKdfType: (value: KdfType, options?: StorageOptions) => Promise; + getKeyHash: (options?: StorageOptions) => Promise; + setKeyHash: (value: string, options?: StorageOptions) => Promise; + getLastActive: (options?: StorageOptions) => Promise; + setLastActive: (value: number, options?: StorageOptions) => Promise; + getLastSync: (options?: StorageOptions) => Promise; + setLastSync: (value: string, options?: StorageOptions) => Promise; + getLegacyEtmKey: (options?: StorageOptions) => Promise; + setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; + getLocalData: (options?: StorageOptions) => Promise; + setLocalData: (value: string, options?: StorageOptions) => Promise; + getLocale: (options?: StorageOptions) => Promise; + setLocale: (value: string, options?: StorageOptions) => Promise; + getLoginRedirect: (options?: StorageOptions) => Promise; + setLoginRedirect: (value: any, options?: StorageOptions) => Promise; + getMainWindowSize: (options?: StorageOptions) => Promise; + setMainWindowSize: (value: number, options?: StorageOptions) => Promise; + getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; + setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; + getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>; + setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise; + getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise; + setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise; + getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise; + setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise; + getOpenAtLogin: (options?: StorageOptions) => Promise; + setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; + getOrganizationInvitation: (options?: StorageOptions) => Promise; + setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; + getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>; + setOrganizations: (value: { [id: string]: OrganizationData }, options?: StorageOptions) => Promise; + getPasswordGenerationOptions: (options?: StorageOptions) => Promise; + setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise; + getProtectedPin: (options?: StorageOptions) => Promise; + setProtectedPin: (value: string, options?: StorageOptions) => Promise; + getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>; + setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise; + getPublicKey: (options?: StorageOptions) => Promise; + setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; + getRefreshToken: (options?: StorageOptions) => Promise; + setRefreshToken: (value: string, options?: StorageOptions) => Promise; + getRememberedEmail: (options?: StorageOptions) => Promise; + setRememberedEmail: (value: string, options?: StorageOptions) => Promise; + getSecurityStamp: (options?: StorageOptions) => Promise; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise; + getSettings: (options?: StorageOptions) => Promise; + setSettings: (value: string, options?: StorageOptions) => Promise; + getSsoCodeVerifier: (options?: StorageOptions) => Promise; + setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise; + getSsoOrgIdentifier: (options?: StorageOptions) => Promise; + setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise; + getSsoState: (options?: StorageOptions) => Promise; + setSsoState: (value: string, options?: StorageOptions) => Promise; + getTheme: (options?: StorageOptions) => Promise; + setTheme: (value: string, options?: StorageOptions) => Promise; + getTwoFactorToken: (options?: StorageOptions) => Promise; + setTwoFactorToken: (value: string, options?: StorageOptions) => Promise; + getUserId: (options?: StorageOptions) => Promise; + getUsesKeyConnector: (options?: StorageOptions) => Promise; + setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise; + getVaultTimeout: (options?: StorageOptions) => Promise; + setVaultTimeout: (value: number, options?: StorageOptions) => Promise; + getVaultTimeoutAction: (options?: StorageOptions) => Promise; + setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; + getStateVersion: () => Promise; + setStateVersion: (value: number) => Promise; + getWindow: () => Promise>; + setWindow: (value: Map) => Promise; } + diff --git a/common/src/abstractions/stateMigration.service.ts b/common/src/abstractions/stateMigration.service.ts new file mode 100644 index 0000000000..f367a9ed24 --- /dev/null +++ b/common/src/abstractions/stateMigration.service.ts @@ -0,0 +1,4 @@ +export abstract class StateMigrationService { + needsMigration: () => Promise; + migrate: () => Promise; +} diff --git a/common/src/abstractions/storage.service.ts b/common/src/abstractions/storage.service.ts index cfedb2d05b..6599267ac1 100644 --- a/common/src/abstractions/storage.service.ts +++ b/common/src/abstractions/storage.service.ts @@ -1,12 +1,9 @@ +import { StorageOptions } from '../models/domain/storageOptions'; + export abstract class StorageService { - get: (key: string, options?: StorageServiceOptions) => Promise; - has: (key: string, options?: StorageServiceOptions) => Promise; - save: (key: string, obj: any, options?: StorageServiceOptions) => Promise; - remove: (key: string, options?: StorageServiceOptions) => Promise; + get: (key: string, options?: StorageOptions) => Promise; + has: (key: string, options?: StorageOptions) => Promise; + save: (key: string, obj: any, options?: StorageOptions) => Promise; + remove: (key: string, options?: StorageOptions) => Promise; } -export interface StorageServiceOptions { - keySuffix: KeySuffixOptions; -} - -export type KeySuffixOptions = 'auto' | 'biometric'; diff --git a/common/src/abstractions/sync.service.ts b/common/src/abstractions/sync.service.ts index 5d35cf20bc..31cb0d259c 100644 --- a/common/src/abstractions/sync.service.ts +++ b/common/src/abstractions/sync.service.ts @@ -8,7 +8,7 @@ export abstract class SyncService { syncInProgress: boolean; getLastSync: () => Promise; - setLastSync: (date: Date) => Promise; + setLastSync: (date: Date, userId?: string) => Promise; fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; syncDeleteFolder: (notification: SyncFolderNotification) => Promise; diff --git a/common/src/abstractions/system.service.ts b/common/src/abstractions/system.service.ts index c8948bc11b..203233f2ea 100644 --- a/common/src/abstractions/system.service.ts +++ b/common/src/abstractions/system.service.ts @@ -1,6 +1,6 @@ export abstract class SystemService { - startProcessReload: () => void; + startProcessReload: () => Promise; cancelProcessReload: () => void; - clearClipboard: (clipboardValue: string, timeoutMs?: number) => void; + clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise; clearPendingClipboard: () => Promise; } diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts index 73c960ec34..e4e2eb0286 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -1,7 +1,4 @@ export abstract class TokenService { - token: string; - decodedToken: any; - refreshToken: string; setTokens: (accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]) => Promise; setToken: (token: string) => Promise; getToken: () => Promise; @@ -15,16 +12,16 @@ export abstract class TokenService { setTwoFactorToken: (token: string, email: string) => Promise; getTwoFactorToken: (email: string) => Promise; clearTwoFactorToken: (email: string) => Promise; - clearToken: () => Promise; - decodeToken: () => any; - getTokenExpirationDate: () => Date; - tokenSecondsRemaining: (offsetSeconds?: number) => number; - tokenNeedsRefresh: (minutes?: number) => boolean; - getUserId: () => string; - getEmail: () => string; - getEmailVerified: () => boolean; - getName: () => string; - getPremium: () => boolean; - getIssuer: () => string; - getIsExternal: () => boolean; + clearToken: (userId?: string) => Promise; + decodeToken: (token?: string) => any; + getTokenExpirationDate: () => Promise; + tokenSecondsRemaining: (offsetSeconds?: number) => Promise; + tokenNeedsRefresh: (minutes?: number) => Promise; + getUserId: () => Promise; + getEmail: () => Promise; + getEmailVerified: () => Promise; + getName: () => Promise; + getPremium: () => Promise; + getIssuer: () => Promise; + getIsExternal: () => Promise; } diff --git a/common/src/abstractions/user.service.ts b/common/src/abstractions/user.service.ts deleted file mode 100644 index 8170ae73a7..0000000000 --- a/common/src/abstractions/user.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OrganizationData } from '../models/data/organizationData'; -import { ProviderData } from '../models/data/providerData'; - -import { Organization } from '../models/domain/organization'; -import { Provider } from '../models/domain/provider'; - -import { KdfType } from '../enums/kdfType'; - -export abstract class UserService { - setInformation: (userId: string, email: string, kdf: KdfType, kdfIterations: number) => Promise; - setEmailVerified: (emailVerified: boolean) => Promise; - setSecurityStamp: (stamp: string) => Promise; - setForcePasswordReset: (forcePasswordReset: boolean) => Promise; - getUserId: () => Promise; - getEmail: () => Promise; - getSecurityStamp: () => Promise; - getKdf: () => Promise; - getKdfIterations: () => Promise; - getEmailVerified: () => Promise; - getForcePasswordReset: () => Promise; - clear: () => Promise; - isAuthenticated: () => Promise; - canAccessPremium: () => Promise; - canManageSponsorships: () => Promise; - getOrganization: (id: string) => Promise; - getOrganizationByIdentifier: (identifier: string) => Promise; - getAllOrganizations: () => Promise; - replaceOrganizations: (organizations: { [id: string]: OrganizationData; }) => Promise; - clearOrganizations: (userId: string) => Promise; - getProvider: (id: string) => Promise; - getAllProviders: () => Promise; - replaceProviders: (providers: { [id: string]: ProviderData; }) => Promise; - clearProviders: (userId: string) => Promise; -} diff --git a/common/src/abstractions/vaultTimeout.service.ts b/common/src/abstractions/vaultTimeout.service.ts index a5944c4813..0ac2ee8630 100644 --- a/common/src/abstractions/vaultTimeout.service.ts +++ b/common/src/abstractions/vaultTimeout.service.ts @@ -1,16 +1,11 @@ -import { EncString } from '../models/domain/encString'; - export abstract class VaultTimeoutService { - biometricLocked: boolean; - everBeenUnlocked: boolean; - pinProtectedKey: EncString; - isLocked: () => Promise; + isLocked: (userId?: string) => Promise; checkVaultTimeout: () => Promise; - lock: (allowSoftLock?: boolean) => Promise; - logOut: () => Promise; + lock: (allowSoftLock?: boolean, userId?: string) => Promise; + logOut: (userId?: string) => Promise; setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; getVaultTimeout: () => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; isBiometricLockSet: () => Promise; - clear: () => Promise; + clear: (userId?: string) => Promise; } diff --git a/common/src/enums/authenticationStatus.ts b/common/src/enums/authenticationStatus.ts new file mode 100644 index 0000000000..83058b2131 --- /dev/null +++ b/common/src/enums/authenticationStatus.ts @@ -0,0 +1,6 @@ +export enum AuthenticationStatus { + Locked = 'locked', + Unlocked = 'unlocked', + LoggedOut = 'loggedOut', + Active = 'active', +} diff --git a/common/src/enums/htmlStorageLocation.ts b/common/src/enums/htmlStorageLocation.ts new file mode 100644 index 0000000000..59560c36a9 --- /dev/null +++ b/common/src/enums/htmlStorageLocation.ts @@ -0,0 +1,5 @@ +export enum HtmlStorageLocation { + Local = 'local', + Memory = 'memory', + Session = 'session', +} diff --git a/common/src/enums/keySuffixOptions.ts b/common/src/enums/keySuffixOptions.ts new file mode 100644 index 0000000000..40d3e58c60 --- /dev/null +++ b/common/src/enums/keySuffixOptions.ts @@ -0,0 +1,4 @@ +export enum KeySuffixOptions { + Auto = 'auto', + Biometric = 'biometric', +} diff --git a/common/src/enums/storageLocation.ts b/common/src/enums/storageLocation.ts new file mode 100644 index 0000000000..d45e5fa695 --- /dev/null +++ b/common/src/enums/storageLocation.ts @@ -0,0 +1,5 @@ +export enum StorageLocation { + Both = 'both', + Disk = 'disk', + Memory = 'memory', +} diff --git a/common/src/models/domain/account.ts b/common/src/models/domain/account.ts new file mode 100644 index 0000000000..eb53d662b7 --- /dev/null +++ b/common/src/models/domain/account.ts @@ -0,0 +1,168 @@ +import { OrganizationData } from '../data/organizationData'; + +import { AuthenticationStatus } from '../../enums/authenticationStatus'; +import { KdfType } from '../../enums/kdfType'; +import { UriMatchType } from '../../enums/uriMatchType'; + +import { CipherView } from '../view/cipherView'; +import { CollectionView } from '../view/collectionView'; +import { FolderView } from '../view/folderView'; +import { SendView } from '../view/sendView'; + +import { EncString } from './encString'; +import { GeneratedPasswordHistory } from './generatedPasswordHistory'; +import { Policy } from './policy'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +import { CipherData } from '../data/cipherData'; +import { CollectionData } from '../data/collectionData'; +import { EventData } from '../data/eventData'; +import { FolderData } from '../data/folderData'; +import { PolicyData } from '../data/policyData'; +import { ProviderData } from '../data/providerData'; +import { SendData } from '../data/sendData'; + +export class EncryptionPair { + encrypted?: TEncrypted; + decrypted?: TDecrypted; +} + +export class DataEncryptionPair { + encrypted?: { [id: string]: TEncrypted }; + decrypted?: TDecrypted[]; +} + +export class AccountData { + ciphers?: DataEncryptionPair = new DataEncryptionPair(); + folders?: DataEncryptionPair = new DataEncryptionPair(); + localData?: any; + sends?: DataEncryptionPair = new DataEncryptionPair(); + collections?: DataEncryptionPair = new DataEncryptionPair(); + policies?: DataEncryptionPair = new DataEncryptionPair(); + passwordGenerationHistory?: EncryptionPair = new EncryptionPair(); + addEditCipherInfo?: any; + collapsedGroupings?: Set; + eventCollection?: EventData[]; + organizations?: { [id: string]: OrganizationData }; + providers?: { [id: string]: ProviderData }; +} + +export class AccountKeys { + cryptoMasterKey?: SymmetricCryptoKey; + cryptoMasterKeyAuto?: string; + cryptoMasterKeyB64?: string; + cryptoMasterKeyBiometric?: string; + cryptoSymmetricKey?: EncryptionPair = new EncryptionPair(); + organizationKeys?: EncryptionPair> = new EncryptionPair>(); + providerKeys?: EncryptionPair> = new EncryptionPair>(); + privateKey?: EncryptionPair = new EncryptionPair(); + legacyEtmKey?: SymmetricCryptoKey; + publicKey?: ArrayBuffer; + apiKeyClientSecret?: string; +} + +export class AccountProfile { + apiKeyClientId?: string; + authenticationStatus?: AuthenticationStatus; + convertAccountToKeyConnector?: boolean; + email?: string; + emailVerified?: boolean; + entityId?: string; + entityType?: string; + everBeenUnlocked?: boolean; + forcePasswordReset?: boolean; + hasPremiumPersonally?: boolean; + lastActive?: number; + lastSync?: string; + ssoCodeVerifier?: string; + ssoOrganizationIdentifier?: string; + ssoState?: string; + userId?: string; + usesKeyConnector?: boolean; + keyHash?: string; + kdfIterations?: number; + kdfType?: KdfType; +} + +export class AccountSettings { + alwaysShowDock?: boolean; + autoConfirmFingerPrints?: boolean; + autoFillOnPageLoadDefault?: boolean; + biometricLocked?: boolean; + biometricUnlock?: boolean; + clearClipboard?: number; + defaultUriMatch?: UriMatchType; + disableAddLoginNotification?: boolean; + disableAutoBiometricsPrompt?: boolean; + disableAutoTotpCopy?: boolean; + disableBadgeCounter?: boolean; + disableChangedPasswordNotification?: boolean; + disableContextMenuItem?: boolean; + disableGa?: boolean; + dontShowCardsCurrentTab?: boolean; + dontShowIdentitiesCurrentTab?: boolean; + enableAlwaysOnTop?: boolean; + enableAutoFillOnPageLoad?: boolean; + enableBiometric?: boolean; + enableBrowserIntegration?: boolean; + enableBrowserIntegrationFingerprint?: boolean; + enableCloseToTray?: boolean; + enableFullWidth?: boolean; + enableGravitars?: boolean; + enableMinimizeToTray?: boolean; + enableStartToTray?: boolean; + enableTray?: boolean; + environmentUrls?: any = { + server: 'bitwarden.com', + }; + equivalentDomains?: any; + minimizeOnCopyToClipboard?: boolean; + neverDomains?: { [id: string]: any }; + openAtLogin?: boolean; + passwordGenerationOptions?: any; + pinProtected?: EncryptionPair = new EncryptionPair(); + protectedPin?: string; + settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly + vaultTimeout?: number; + vaultTimeoutAction?: string; +} + +export class AccountTokens { + accessToken?: string; + decodedToken?: any; + refreshToken?: string; + securityStamp?: string; +} + +export class Account { + data?: AccountData = new AccountData(); + keys?: AccountKeys = new AccountKeys(); + profile?: AccountProfile = new AccountProfile(); + settings?: AccountSettings = new AccountSettings(); + tokens?: AccountTokens = new AccountTokens(); + + constructor(init: Partial) { + Object.assign(this, { + data: { + ...new AccountData(), + ...init?.data, + }, + keys: { + ...new AccountKeys(), + ...init?.keys, + }, + profile: { + ...new AccountProfile(), + ...init?.profile, + }, + settings: { + ...new AccountSettings(), + ...init?.settings, + }, + tokens: { + ...new AccountTokens(), + ...init?.tokens, + }, + }); + } +} diff --git a/common/src/models/domain/globalState.ts b/common/src/models/domain/globalState.ts new file mode 100644 index 0000000000..f0fa78ced5 --- /dev/null +++ b/common/src/models/domain/globalState.ts @@ -0,0 +1,24 @@ +export class GlobalState { + enableAlwaysOnTop?: boolean; + installedVersion?: string; + lastActive?: number; + locale?: string; + openAtLogin?: boolean; + organizationInvitation?: any; + rememberedEmail?: string; + theme?: string; + window?: Map = new Map(); + twoFactorToken?: string; + disableFavicon?: boolean; + biometricAwaitingAcceptance?: boolean; + biometricFingerprintValidated?: boolean; + vaultTimeout?: number; + vaultTimeoutAction?: string; + loginRedirect?: any; + mainWindowSize?: number; + enableBiometrics?: boolean; + biometricText?: string; + noAutoPromptBiometrics?: boolean; + noAutoPromptBiometricsText?: string; + stateVersion: number; +} diff --git a/common/src/models/domain/state.ts b/common/src/models/domain/state.ts new file mode 100644 index 0000000000..a0099e5d24 --- /dev/null +++ b/common/src/models/domain/state.ts @@ -0,0 +1,9 @@ +import { Account } from './account'; +import { GlobalState } from './globalState'; + +export class State { + accounts: { [userId: string]: Account } = {}; + globals: GlobalState = new GlobalState(); + activeUserId: string; +} + diff --git a/common/src/models/domain/storageOptions.ts b/common/src/models/domain/storageOptions.ts new file mode 100644 index 0000000000..d9924795d6 --- /dev/null +++ b/common/src/models/domain/storageOptions.ts @@ -0,0 +1,10 @@ +import { HtmlStorageLocation } from '../../enums/htmlStorageLocation'; +import { StorageLocation } from '../../enums/storageLocation'; + +export type StorageOptions = { + storageLocation?: StorageLocation; + useSecureStorage?: boolean; + userId?: string; + htmlStorageLocation?: HtmlStorageLocation; + keySuffix?: string, +}; diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index 46fdc1391e..f42a760e0b 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -1515,7 +1515,7 @@ export class ApiService implements ApiServiceAbstraction { async getActiveBearerToken(): Promise { let accessToken = await this.tokenService.getToken(); - if (this.tokenService.tokenNeedsRefresh()) { + if (await this.tokenService.tokenNeedsRefresh()) { await this.doAuthRefresh(); accessToken = await this.tokenService.getToken(); } @@ -1637,7 +1637,7 @@ export class ApiService implements ApiServiceAbstraction { headers.set('User-Agent', this.customUserAgent); } - const decodedToken = this.tokenService.decodeToken(); + const decodedToken = await this.tokenService.decodeToken(); const response = await this.fetch(new Request(this.environmentService.getIdentityUrl() + '/connect/token', { body: this.qsStringify({ grant_type: 'refresh_token', diff --git a/common/src/services/apiKey.service.ts b/common/src/services/apiKey.service.ts deleted file mode 100644 index 0b67781b20..0000000000 --- a/common/src/services/apiKey.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ApiKeyService as ApiKeyServiceAbstraction } from '../abstractions/apiKey.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; - -import { Utils } from '../misc/utils'; - -const Keys = { - clientId: 'clientId', - clientSecret: 'clientSecret', - entityType: 'entityType', - entityId: 'entityId', -}; - - -export class ApiKeyService implements ApiKeyServiceAbstraction { - private clientId: string; - private clientSecret: string; - private entityType: string; - private entityId: string; - - constructor(private tokenService: TokenService, private storageService: StorageService) { } - - async setInformation(clientId: string, clientSecret: string) { - this.clientId = clientId; - this.clientSecret = clientSecret; - const idParts = clientId.split('.'); - - if (idParts.length !== 2 || !Utils.isGuid(idParts[1])) { - throw Error('Invalid clientId'); - } - this.entityType = idParts[0]; - this.entityId = idParts[1]; - - await this.storageService.save(Keys.clientId, this.clientId); - await this.storageService.save(Keys.entityId, this.entityId); - await this.storageService.save(Keys.entityType, this.entityType); - await this.storageService.save(Keys.clientSecret, this.clientSecret); - } - - async getClientId(): Promise { - if (this.clientId == null) { - this.clientId = await this.storageService.get(Keys.clientId); - } - return this.clientId; - } - - async getClientSecret(): Promise { - if (this.clientSecret == null) { - this.clientSecret = await this.storageService.get(Keys.clientSecret); - } - return this.clientSecret; - } - - async getEntityType(): Promise { - if (this.entityType == null) { - this.entityType = await this.storageService.get(Keys.entityType); - } - return this.entityType; - } - - async getEntityId(): Promise { - if (this.entityId == null) { - this.entityId = await this.storageService.get(Keys.entityId); - } - return this.entityId; - } - - async clear(): Promise { - await this.storageService.remove(Keys.clientId); - await this.storageService.remove(Keys.clientSecret); - await this.storageService.remove(Keys.entityId); - await this.storageService.remove(Keys.entityType); - - this.clientId = this.clientSecret = this.entityId = this.entityType = null; - } - - async isAuthenticated(): Promise { - const token = await this.tokenService.getToken(); - if (token == null) { - return false; - } - - const entityId = await this.getEntityId(); - return entityId != null; - } -} diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index e4f670d7b7..540d7e5c28 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -2,6 +2,7 @@ import { HashPurpose } from '../enums/hashPurpose'; import { KdfType } from '../enums/kdfType'; import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; +import { Account, AccountData, AccountProfile, AccountTokens } from '../models/domain/account'; import { AuthResult } from '../models/domain/authResult'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; @@ -26,8 +27,8 @@ import { KeyConnectorService } from '../abstractions/keyConnector.service'; import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StateService } from '../abstractions/state.service'; import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; import { Utils } from '../misc/utils'; @@ -99,12 +100,12 @@ export class AuthService implements AuthServiceAbstraction { private key: SymmetricCryptoKey; constructor(private cryptoService: CryptoService, protected apiService: ApiService, - private userService: UserService, protected tokenService: TokenService, - protected appIdService: AppIdService, private i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private logService: LogService, - private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService, - private keyConnectorService: KeyConnectorService, private setCryptoKeys = true) { + protected tokenService: TokenService, protected appIdService: AppIdService, + private i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + private messagingService: MessagingService, private vaultTimeoutService: VaultTimeoutService, + private logService: LogService, protected cryptoFunctionService: CryptoFunctionService, + private keyConnectorService: KeyConnectorService, protected environmentService: EnvironmentService, + protected stateService: StateService, private setCryptoKeys = true) { } init() { @@ -234,7 +235,7 @@ export class AuthService implements AuthServiceAbstraction { let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProvidersData.forEach((value, type) => { + this.twoFactorProvidersData.forEach((_value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { @@ -350,13 +351,34 @@ export class AuthService implements AuthServiceAbstraction { const tokenResponse = response as IdentityTokenResponse; result.resetMasterPassword = tokenResponse.resetMasterPassword; result.forcePasswordReset = tokenResponse.forcePasswordReset; + + const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken); + await this.stateService.addAccount({ + profile: { + ...new AccountProfile(), + ...{ + userId: accountInformation.sub, + email: accountInformation.email, + apiKeyClientId: clientId, + apiKeyClientSecret: clientSecret, + hasPremiumPersonally: accountInformation.premium, + kdfIterations: tokenResponse.kdfIterations, + kdfType: tokenResponse.kdf, + }, + }, + tokens: { + ...new AccountTokens(), + ...{ + accessToken: tokenResponse.accessToken, + refreshToken: tokenResponse.refreshToken, + }, + }, + }); + if (tokenResponse.twoFactorToken != null) { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, clientIdClientSecret); - await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), - tokenResponse.kdf, tokenResponse.kdfIterations); if (this.setCryptoKeys) { if (key != null) { await this.cryptoService.setKey(key); @@ -392,7 +414,7 @@ export class AuthService implements AuthServiceAbstraction { } else if (tokenResponse.keyConnectorUrl != null) { const password = await this.cryptoFunctionService.randomBytes(64); - const k = await this.cryptoService.makeKey(Utils.fromBufferToB64(password), this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations); + const k = await this.cryptoService.makeKey(Utils.fromBufferToB64(password), await this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations); const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64); await this.cryptoService.setKey(k); @@ -416,7 +438,7 @@ export class AuthService implements AuthServiceAbstraction { } if (this.vaultTimeoutService != null) { - this.vaultTimeoutService.biometricLocked = false; + await this.stateService.setBiometricLocked(false); } this.messagingService.send('loggedIn'); return result; diff --git a/common/src/services/cipher.service.ts b/common/src/services/cipher.service.ts index 421f381ac6..9ee245dd31 100644 --- a/common/src/services/cipher.service.ts +++ b/common/src/services/cipher.service.ts @@ -46,43 +46,32 @@ import { FileUploadService } from '../abstractions/fileUpload.service'; import { I18nService } from '../abstractions/i18n.service'; import { SearchService } from '../abstractions/search.service'; import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; - -import { ConstantsService } from './constants.service'; +import { StateService } from '../abstractions/state.service'; import { LogService } from '../abstractions/log.service'; import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; -const Keys = { - ciphersPrefix: 'ciphers_', - localData: 'sitesLocalData', - neverDomains: 'neverDomains', -}; - const DomainMatchBlacklist = new Map>([ ['google.com', new Set(['script.google.com'])], ]); export class CipherService implements CipherServiceAbstraction { - // tslint:disable-next-line - _decryptedCipherCache: CipherView[]; - private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(this.sortCiphersByLastUsed); - constructor(private cryptoService: CryptoService, private userService: UserService, - private settingsService: SettingsService, private apiService: ApiService, - private fileUploadService: FileUploadService, private storageService: StorageService, + constructor(private cryptoService: CryptoService, private settingsService: SettingsService, + private apiService: ApiService, private fileUploadService: FileUploadService, private i18nService: I18nService, private searchService: () => SearchService, - private logService: LogService) { + private logService: LogService, private stateService: StateService) { } - get decryptedCipherCache() { - return this._decryptedCipherCache; + async getDecryptedCipherCache(): Promise { + const decryptedCiphers = await this.stateService.getDecryptedCiphers(); + return decryptedCiphers; } - set decryptedCipherCache(value: CipherView[]) { - this._decryptedCipherCache = value; + + async setDecryptedCipherCache(value: CipherView[]) { + await this.stateService.setDecryptedCiphers(value); if (this.searchService != null) { if (value == null) { this.searchService().clearIndex(); @@ -92,9 +81,8 @@ export class CipherService implements CipherServiceAbstraction { } } - clearCache(): void { - this.decryptedCipherCache = null; - this.sortedCiphersCache.clear(); + async clearCache(userId?: string): Promise { + await this.clearDecryptedCiphersState(userId); } async encrypt(model: CipherView, key?: SymmetricCryptoKey, originalCipher: Cipher = null): Promise { @@ -212,12 +200,10 @@ export class CipherService implements CipherServiceAbstraction { const self = this; const encFields: Field[] = []; - await fieldsModel.reduce((promise, field) => { - return promise.then(() => { - return self.encryptField(field, key); - }).then((encField: Field) => { - encFields.push(encField); - }); + await fieldsModel.reduce(async (promise, field) => { + await promise; + const encField = await self.encryptField(field, key); + encFields.push(encField); }, Promise.resolve()); return encFields; @@ -247,12 +233,10 @@ export class CipherService implements CipherServiceAbstraction { const self = this; const encPhs: Password[] = []; - await phModels.reduce((promise, ph) => { - return promise.then(() => { - return self.encryptPasswordHistory(ph, key); - }).then((encPh: Password) => { - encPhs.push(encPh); - }); + await phModels.reduce(async (promise, ph) => { + await promise; + const encPh = await self.encryptPasswordHistory(ph, key); + encPhs.push(encPh); }, Promise.resolve()); return encPhs; @@ -270,22 +254,18 @@ export class CipherService implements CipherServiceAbstraction { } async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const localData = await this.storageService.get(Keys.localData); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null || !ciphers.hasOwnProperty(id)) { return null; } + const localData = await this.stateService.getLocalData(); return new Cipher(ciphers[id], false, localData ? localData[id] : null); } async getAll(): Promise { - const userId = await this.userService.getUserId(); - const localData = await this.storageService.get(Keys.localData); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const localData = await this.stateService.getLocalData(); + const ciphers = await this.stateService.getEncryptedCiphers(); const response: Cipher[] = []; for (const id in ciphers) { if (ciphers.hasOwnProperty(id)) { @@ -297,13 +277,13 @@ export class CipherService implements CipherServiceAbstraction { @sequentialize(() => 'getAllDecrypted') async getAllDecrypted(): Promise { - if (this.decryptedCipherCache != null) { - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); + if (await this.getDecryptedCipherCache() != null) { if (this.searchService != null && (this.searchService().indexedEntityId ?? userId) !== userId) { - await this.searchService().indexCiphers(userId, this.decryptedCipherCache); + await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache()); } - return this.decryptedCipherCache; + return await this.getDecryptedCipherCache(); } const decCiphers: CipherView[] = []; @@ -314,14 +294,14 @@ export class CipherService implements CipherServiceAbstraction { const promises: any[] = []; const ciphers = await this.getAll(); - ciphers.forEach(cipher => { + ciphers.forEach(async cipher => { promises.push(cipher.decrypt().then(c => decCiphers.push(c))); }); await Promise.all(promises); decCiphers.sort(this.getLocaleSortingFunction()); - this.decryptedCipherCache = decCiphers; - return this.decryptedCipherCache; + await this.stateService.setDecryptedCiphers(decCiphers); + return decCiphers; } async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { @@ -369,7 +349,7 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = result[1]; if (defaultMatch == null) { - defaultMatch = await this.storageService.get(ConstantsService.defaultUriMatch); + defaultMatch = await this.stateService.getDefaultUriMatch(); if (defaultMatch == null) { defaultMatch = UriMatchType.Domain; } @@ -476,7 +456,7 @@ export class CipherService implements CipherServiceAbstraction { } async updateLastUsedDate(id: string): Promise { - let ciphersLocalData = await this.storageService.get(Keys.localData); + let ciphersLocalData = await this.stateService.getLocalData(); if (!ciphersLocalData) { ciphersLocalData = {}; } @@ -489,23 +469,25 @@ export class CipherService implements CipherServiceAbstraction { }; } - await this.storageService.save(Keys.localData, ciphersLocalData); + await this.stateService.setLocalData(ciphersLocalData); - if (this.decryptedCipherCache == null) { + const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + if (!decryptedCipherCache) { return; } - for (let i = 0; i < this.decryptedCipherCache.length; i++) { - const cached = this.decryptedCipherCache[i]; + for (let i = 0; i < decryptedCipherCache.length; i++) { + const cached = decryptedCipherCache[i]; if (cached.id === id) { cached.localData = ciphersLocalData[id]; break; } } + await this.stateService.setDecryptedCiphers(decryptedCipherCache); } async updateLastLaunchedDate(id: string): Promise { - let ciphersLocalData = await this.storageService.get(Keys.localData); + let ciphersLocalData = await this.stateService.getLocalData(); if (!ciphersLocalData) { ciphersLocalData = {}; } @@ -518,19 +500,21 @@ export class CipherService implements CipherServiceAbstraction { }; } - await this.storageService.save(Keys.localData, ciphersLocalData); + await this.stateService.setLocalData(ciphersLocalData); - if (this.decryptedCipherCache == null) { + const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + if (!decryptedCipherCache) { return; } - for (let i = 0; i < this.decryptedCipherCache.length; i++) { - const cached = this.decryptedCipherCache[i]; + for (let i = 0; i < decryptedCipherCache.length; i++) { + const cached = decryptedCipherCache[i]; if (cached.id === id) { cached.localData = ciphersLocalData[id]; break; } } + await this.stateService.setDecryptedCiphers(decryptedCipherCache); } async saveNeverDomain(domain: string): Promise { @@ -538,12 +522,12 @@ export class CipherService implements CipherServiceAbstraction { return; } - let domains = await this.storageService.get<{ [id: string]: any; }>(Keys.neverDomains); + let domains = await this.stateService.getNeverDomains(); if (!domains) { domains = {}; } domains[domain] = null; - await this.storageService.save(Keys.neverDomains, domains); + await this.stateService.setNeverDomains(domains); } async saveWithServer(cipher: Cipher): Promise { @@ -562,8 +546,7 @@ export class CipherService implements CipherServiceAbstraction { response = await this.apiService.putCipher(cipher.id, request); } - const userId = await this.userService.getUserId(); - const data = new CipherData(response, userId, cipher.collectionIds); + const data = new CipherData(response, await this.stateService.getUserId(), cipher.collectionIds); await this.upsert(data); } @@ -583,8 +566,7 @@ export class CipherService implements CipherServiceAbstraction { const encCipher = await this.encrypt(cipher); const request = new CipherShareRequest(encCipher); const response = await this.apiService.putShareCipher(cipher.id, request); - const userId = await this.userService.getUserId(); - const data = new CipherData(response, userId, collectionIds); + const data = new CipherData(response, await this.stateService.getUserId(), collectionIds); await this.upsert(data); } @@ -601,7 +583,7 @@ export class CipherService implements CipherServiceAbstraction { await Promise.all(promises); const request = new CipherBulkShareRequest(encCiphers, collectionIds); await this.apiService.putShareCiphers(request); - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); await this.upsert(encCiphers.map(c => c.toCipherData(userId))); } @@ -618,7 +600,7 @@ export class CipherService implements CipherServiceAbstraction { reject(e); } }; - reader.onerror = evt => { + reader.onerror = _evt => { reject('Error reading file.'); }; }); @@ -654,8 +636,7 @@ export class CipherService implements CipherServiceAbstraction { } } - const userId = await this.userService.getUserId(); - const cData = new CipherData(response, userId, cipher.collectionIds); + const cData = new CipherData(response, await this.stateService.getUserId(), cipher.collectionIds); if (!admin) { await this.upsert(cData); } @@ -702,15 +683,12 @@ export class CipherService implements CipherServiceAbstraction { async saveCollectionsWithServer(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); await this.apiService.putCipherCollections(cipher.id, request); - const userId = await this.userService.getUserId(); - const data = cipher.toCipherData(userId); + const data = cipher.toCipherData(await this.stateService.getUserId()); await this.upsert(data); } async upsert(cipher: CipherData | CipherData[]): Promise { - const userId = await this.userService.getUserId(); - let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + let ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { ciphers = {}; } @@ -724,27 +702,23 @@ export class CipherService implements CipherServiceAbstraction { }); } - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.replace(ciphers); } async replace(ciphers: { [id: string]: CipherData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearDecryptedCiphersState(); + await this.stateService.setEncryptedCiphers(ciphers); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.ciphersPrefix + userId); - this.clearCache(); + async clear(userId?: string): Promise { + await this.clearEncryptedCiphersState(userId); + await this.clearCache(userId); } async moveManyWithServer(ids: string[], folderId: string): Promise { await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - const userId = await this.userService.getUserId(); - let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + let ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { ciphers = {}; } @@ -755,14 +729,12 @@ export class CipherService implements CipherServiceAbstraction { } }); - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); } async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { return; } @@ -778,8 +750,8 @@ export class CipherService implements CipherServiceAbstraction { }); } - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); } async deleteWithServer(id: string): Promise { @@ -793,9 +765,7 @@ export class CipherService implements CipherServiceAbstraction { } async deleteAttachment(id: string, attachmentId: string): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { return; @@ -807,8 +777,8 @@ export class CipherService implements CipherServiceAbstraction { } } - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); } async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { @@ -887,9 +857,7 @@ export class CipherService implements CipherServiceAbstraction { } async softDelete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { return; } @@ -907,8 +875,8 @@ export class CipherService implements CipherServiceAbstraction { (id as string[]).forEach(setDeletedDate); } - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); } async softDeleteWithServer(id: string): Promise { @@ -922,9 +890,7 @@ export class CipherService implements CipherServiceAbstraction { } async restore(cipher: { id: string, revisionDate: string; } | { id: string, revisionDate: string; }[]) { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { return; } @@ -944,8 +910,8 @@ export class CipherService implements CipherServiceAbstraction { clearDeletedDate(cipher as { id: string, revisionDate: string; }); } - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); } async restoreWithServer(id: string): Promise { @@ -1109,7 +1075,7 @@ export class CipherService implements CipherServiceAbstraction { } if (autofillOnPageLoad) { - const autofillOnPageLoadDefault = await this.storageService.get(ConstantsService.autoFillOnPageLoadDefaultKey); + const autofillOnPageLoadDefault = await this.stateService.getAutoFillOnPageLoadDefault(); ciphers = ciphers.filter(cipher => cipher.login.autofillOnPageLoad || (cipher.login.autofillOnPageLoad == null && autofillOnPageLoadDefault !== false)); if (ciphers.length === 0) { @@ -1128,4 +1094,17 @@ export class CipherService implements CipherServiceAbstraction { return this.sortedCiphersCache.getNext(cacheKey); } } + + private async clearEncryptedCiphersState(userId?: string) { + await this.stateService.setEncryptedCiphers(null, { userId: userId }); + } + + private async clearDecryptedCiphersState(userId?: string) { + await this.stateService.setDecryptedCiphers(null, { userId: userId }); + this.clearSortedCiphers(); + } + + private clearSortedCiphers() { + this.sortedCiphersCache.clear(); + } } diff --git a/common/src/services/collection.service.ts b/common/src/services/collection.service.ts index 3c594080f8..3affccd66e 100644 --- a/common/src/services/collection.service.ts +++ b/common/src/services/collection.service.ts @@ -8,26 +8,20 @@ import { CollectionView } from '../models/view/collectionView'; import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { StateService } from '../abstractions/state.service'; import { ServiceUtils } from '../misc/serviceUtils'; import { Utils } from '../misc/utils'; -const Keys = { - collectionsPrefix: 'collections_', -}; const NestingDelimiter = '/'; export class CollectionService implements CollectionServiceAbstraction { - decryptedCollectionCache: CollectionView[]; - - constructor(private cryptoService: CryptoService, private userService: UserService, - private storageService: StorageService, private i18nService: I18nService) { + constructor(private cryptoService: CryptoService, private i18nService: I18nService, + private stateService: StateService) { } - clearCache(): void { - this.decryptedCollectionCache = null; + async clearCache(userId?: string): Promise { + await this.stateService.setDecryptedCollections(null, { userId: userId }); } async encrypt(model: CollectionView): Promise { @@ -60,9 +54,7 @@ export class CollectionService implements CollectionServiceAbstraction { } async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); + const collections = await this.stateService.getEncryptedCollections(); if (collections == null || !collections.hasOwnProperty(id)) { return null; } @@ -71,9 +63,7 @@ export class CollectionService implements CollectionServiceAbstraction { } async getAll(): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); + const collections = await this.stateService.getEncryptedCollections(); const response: Collection[] = []; for (const id in collections) { if (collections.hasOwnProperty(id)) { @@ -84,8 +74,9 @@ export class CollectionService implements CollectionServiceAbstraction { } async getAllDecrypted(): Promise { - if (this.decryptedCollectionCache != null) { - return this.decryptedCollectionCache; + let decryptedCollections = await this.stateService.getDecryptedCollections(); + if (decryptedCollections != null) { + return decryptedCollections; } const hasKey = await this.cryptoService.hasKey(); @@ -94,8 +85,9 @@ export class CollectionService implements CollectionServiceAbstraction { } const collections = await this.getAll(); - this.decryptedCollectionCache = await this.decryptMany(collections); - return this.decryptedCollectionCache; + decryptedCollections = await this.decryptMany(collections); + await this.stateService.setDecryptedCollections(decryptedCollections); + return decryptedCollections; } async getAllNested(collections: CollectionView[] = null): Promise[]> { @@ -119,9 +111,7 @@ export class CollectionService implements CollectionServiceAbstraction { } async upsert(collection: CollectionData | CollectionData[]): Promise { - const userId = await this.userService.getUserId(); - let collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); + let collections = await this.stateService.getEncryptedCollections(); if (collections == null) { collections = {}; } @@ -135,31 +125,26 @@ export class CollectionService implements CollectionServiceAbstraction { }); } - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; + await this.replace(collections); } async replace(collections: { [id: string]: CollectionData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; + await this.clearCache(); + await this.stateService.setEncryptedCollections(collections); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.collectionsPrefix + userId); - this.decryptedCollectionCache = null; + async clear(userId?: string): Promise { + await this.clearCache(userId); + await this.stateService.setEncryptedCollections(null, { userId: userId }); } async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); + const collections = await this.stateService.getEncryptedCollections(); if (collections == null) { return; } if (typeof id === 'string') { - const i = id as string; delete collections[id]; } else { (id as string[]).forEach(i => { @@ -167,7 +152,6 @@ export class CollectionService implements CollectionServiceAbstraction { }); } - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; + await this.replace(collections); } } diff --git a/common/src/services/constants.service.ts b/common/src/services/constants.service.ts deleted file mode 100644 index 9202f8620b..0000000000 --- a/common/src/services/constants.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -export class ConstantsService { - static readonly environmentUrlsKey: string = 'environmentUrls'; - static readonly disableGaKey: string = 'disableGa'; - static readonly disableAddLoginNotificationKey: string = 'disableAddLoginNotification'; - static readonly disableChangedPasswordNotificationKey: string = 'disableChangedPasswordNotification'; - static readonly disableContextMenuItemKey: string = 'disableContextMenuItem'; - static readonly disableFaviconKey: string = 'disableFavicon'; - static readonly disableBadgeCounterKey: string = 'disableBadgeCounter'; - static readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; - static readonly disableAutoBiometricsPromptKey: string = 'noAutoPromptBiometrics'; - static readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; - static readonly autoFillOnPageLoadDefaultKey: string = 'autoFillOnPageLoadDefault'; - static readonly vaultTimeoutKey: string = 'lockOption'; - static readonly vaultTimeoutActionKey: string = 'vaultTimeoutAction'; - static readonly lastActiveKey: string = 'lastActive'; - static readonly neverDomainsKey: string = 'neverDomains'; - static readonly installedVersionKey: string = 'installedVersion'; - static readonly localeKey: string = 'locale'; - static readonly themeKey: string = 'theme'; - static readonly collapsedGroupingsKey: string = 'collapsedGroupings'; - static readonly autoConfirmFingerprints: string = 'autoConfirmFingerprints'; - static readonly dontShowCardsCurrentTab: string = 'dontShowCardsCurrentTab'; - static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; - static readonly defaultUriMatch: string = 'defaultUriMatch'; - static readonly pinProtectedKey: string = 'pinProtectedKey'; - static readonly protectedPin: string = 'protectedPin'; - static readonly clearClipboardKey: string = 'clearClipboardKey'; - static readonly eventCollectionKey: string = 'eventCollection'; - static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; - static readonly ssoStateKey: string = 'ssoState'; - static readonly biometricUnlockKey: string = 'biometric'; - static readonly biometricText: string = 'biometricText'; - static readonly biometricAwaitingAcceptance: string = 'biometricAwaitingAcceptance'; - static readonly biometricFingerprintValidated: string = 'biometricFingerprintValidated'; - - readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; - readonly disableGaKey: string = ConstantsService.disableGaKey; - readonly disableAddLoginNotificationKey: string = ConstantsService.disableAddLoginNotificationKey; - readonly disableContextMenuItemKey: string = ConstantsService.disableContextMenuItemKey; - readonly disableFaviconKey: string = ConstantsService.disableFaviconKey; - readonly disableBadgeCounterKey: string = ConstantsService.disableBadgeCounterKey; - readonly disableAutoTotpCopyKey: string = ConstantsService.disableAutoTotpCopyKey; - readonly disableAutoBiometricsPromptKey: string = ConstantsService.disableAutoBiometricsPromptKey; - readonly enableAutoFillOnPageLoadKey: string = ConstantsService.enableAutoFillOnPageLoadKey; - readonly autoFillOnPageLoadDefaultKey: string = ConstantsService.autoFillOnPageLoadDefaultKey; - readonly vaultTimeoutKey: string = ConstantsService.vaultTimeoutKey; - readonly vaultTimeoutActionKey: string = ConstantsService.vaultTimeoutActionKey; - readonly lastActiveKey: string = ConstantsService.lastActiveKey; - readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; - readonly installedVersionKey: string = ConstantsService.installedVersionKey; - readonly localeKey: string = ConstantsService.localeKey; - readonly themeKey: string = ConstantsService.themeKey; - readonly collapsedGroupingsKey: string = ConstantsService.collapsedGroupingsKey; - readonly autoConfirmFingerprints: string = ConstantsService.autoConfirmFingerprints; - readonly dontShowCardsCurrentTab: string = ConstantsService.dontShowCardsCurrentTab; - readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; - readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; - readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; - readonly protectedPin: string = ConstantsService.protectedPin; - readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; - readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; - readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; - readonly ssoStateKey: string = ConstantsService.ssoStateKey; - readonly biometricUnlockKey: string = ConstantsService.biometricUnlockKey; - readonly biometricText: string = ConstantsService.biometricText; - readonly biometricAwaitingAcceptance: string = ConstantsService.biometricAwaitingAcceptance; - readonly biometricFingerprintValidated: string = ConstantsService.biometricFingerprintValidated; -} diff --git a/common/src/services/crypto.service.ts b/common/src/services/crypto.service.ts index 60ef0ea055..25989d178f 100644 --- a/common/src/services/crypto.service.ts +++ b/common/src/services/crypto.service.ts @@ -3,84 +3,60 @@ import * as bigInt from 'big-integer'; import { EncryptionType } from '../enums/encryptionType'; import { HashPurpose } from '../enums/hashPurpose'; import { KdfType } from '../enums/kdfType'; +import { KeySuffixOptions } from '../enums/keySuffixOptions'; import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; import { EncryptedObject } from '../models/domain/encryptedObject'; import { EncString } from '../models/domain/encString'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { LogService } from '../abstractions/log.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { - KeySuffixOptions, - StorageService, -} from '../abstractions/storage.service'; - -import { ConstantsService } from './constants.service'; +import { StateService } from '../abstractions/state.service'; import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; import { EEFLongWordList } from '../misc/wordlist'; + +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; import { ProfileProviderOrganizationResponse } from '../models/response/profileProviderOrganizationResponse'; import { ProfileProviderResponse } from '../models/response/profileProviderResponse'; -export const Keys = { - key: 'key', // Master Key - encOrgKeys: 'encOrgKeys', - encProviderKeys: 'encProviderKeys', - encPrivateKey: 'encPrivateKey', - encKey: 'encKey', // Generated Symmetric Key - keyHash: 'keyHash', -}; - export class CryptoService implements CryptoServiceAbstraction { - private key: SymmetricCryptoKey; - private encKey: SymmetricCryptoKey; - private legacyEtmKey: SymmetricCryptoKey; - private keyHash: string; - private publicKey: ArrayBuffer; - private privateKey: ArrayBuffer; - private orgKeys: Map; - private providerKeys: Map; - - constructor(private storageService: StorageService, protected secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService, protected platformUtilService: PlatformUtilsService, - protected logService: LogService) { + constructor(private cryptoFunctionService: CryptoFunctionService, protected platformUtilService: PlatformUtilsService, + protected logService: LogService, protected stateService: StateService) { } - async setKey(key: SymmetricCryptoKey): Promise { - this.key = key; - - await this.storeKey(key); + async setKey(key: SymmetricCryptoKey, userId?: string): Promise { + await this.stateService.setCryptoMasterKey(key, { userId: userId }); + await this.storeKey(key, userId); } - setKeyHash(keyHash: string): Promise<{}> { - this.keyHash = keyHash; - return this.storageService.save(Keys.keyHash, keyHash); + async setKeyHash(keyHash: string): Promise { + await this.stateService.setKeyHash(keyHash); } - async setEncKey(encKey: string): Promise<{}> { + async setEncKey(encKey: string): Promise { if (encKey == null) { return; } - await this.storageService.save(Keys.encKey, encKey); - this.encKey = null; + await this.stateService.setDecryptedCryptoSymmetricKey(null); + await this.stateService.setEncryptedCryptoSymmetricKey(encKey); } - async setEncPrivateKey(encPrivateKey: string): Promise<{}> { + async setEncPrivateKey(encPrivateKey: string): Promise { if (encPrivateKey == null) { return; } - await this.storageService.save(Keys.encPrivateKey, encPrivateKey); - this.privateKey = null; + await this.stateService.setDecryptedPrivateKey(null); + await this.stateService.setEncryptedPrivateKey(encPrivateKey); } - async setOrgKeys(orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]): Promise<{}> { + async setOrgKeys(orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]): Promise { const orgKeys: any = {}; orgs.forEach(org => { orgKeys[org.id] = org.key; @@ -90,47 +66,49 @@ export class CryptoService implements CryptoServiceAbstraction { // Convert provider encrypted keys to user encrypted. const providerKey = await this.getProviderKey(providerOrg.providerId); const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey); - orgKeys[providerOrg.id] = await (await this.rsaEncrypt(decValue)).encryptedString; + orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString; } - this.orgKeys = null; - return this.storageService.save(Keys.encOrgKeys, orgKeys); + await this.stateService.setDecryptedOrganizationKeys(null); + return await this.stateService.setEncryptedOrganizationKeys(orgKeys); } - setProviderKeys(providers: ProfileProviderResponse[]): Promise<{}> { + async setProviderKeys(providers: ProfileProviderResponse[]): Promise { const providerKeys: any = {}; providers.forEach(provider => { providerKeys[provider.id] = provider.key; }); - this.providerKeys = null; - return this.storageService.save(Keys.encProviderKeys, providerKeys); + await this.stateService.setDecryptedProviderKeys(null); + return await this.stateService.setEncryptedProviderKeys(providerKeys); } - async getKey(keySuffix?: KeySuffixOptions): Promise { - if (this.key != null) { - return this.key; + async getKey(keySuffix?: KeySuffixOptions, userId?: string): Promise { + const inMemoryKey = await this.stateService.getCryptoMasterKey({ userId: userId }); + + if (inMemoryKey != null) { + return inMemoryKey; } - keySuffix ||= 'auto'; - const symmetricKey = await this.getKeyFromStorage(keySuffix); + keySuffix ||= KeySuffixOptions.Auto; + const symmetricKey = await this.getKeyFromStorage(keySuffix, userId); if (symmetricKey != null) { - this.setKey(symmetricKey); + // TODO: Refactor here so get key doesn't also set key + this.setKey(symmetricKey, userId); } return symmetricKey; } - async getKeyFromStorage(keySuffix: KeySuffixOptions): Promise { - const key = await this.retrieveKeyFromStorage(keySuffix); + async getKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise { + const key = await this.retrieveKeyFromStorage(keySuffix, userId); if (key != null) { - const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); if (!await this.validateKey(symmetricKey)) { this.logService.warning('Wrong key, throwing away stored key'); - this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix }); + await this.clearSecretKeyStore(userId); return null; } @@ -140,16 +118,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async getKeyHash(): Promise { - if (this.keyHash != null) { - return this.keyHash; - } - - const keyHash = await this.storageService.get(Keys.keyHash); - if (keyHash != null) { - this.keyHash = keyHash; - } - - return keyHash == null ? null : this.keyHash; + return await this.stateService.getKeyHash(); } async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise { @@ -173,11 +142,12 @@ export class CryptoService implements CryptoServiceAbstraction { @sequentialize(() => 'getEncKey') async getEncKey(key: SymmetricCryptoKey = null): Promise { - if (this.encKey != null) { - return this.encKey; + const inMemoryKey = await this.stateService.getDecryptedCryptoSymmetricKey(); + if (inMemoryKey != null) { + return inMemoryKey; } - const encKey = await this.storageService.get(Keys.encKey); + const encKey = await this.stateService.getEncryptedCryptoSymmetricKey(); if (encKey == null) { return null; } @@ -203,13 +173,15 @@ export class CryptoService implements CryptoServiceAbstraction { if (decEncKey == null) { return null; } - this.encKey = new SymmetricCryptoKey(decEncKey); - return this.encKey; + const symmetricCryptoKey = new SymmetricCryptoKey(decEncKey); + await this.stateService.setDecryptedCryptoSymmetricKey(symmetricCryptoKey); + return symmetricCryptoKey; } async getPublicKey(): Promise { - if (this.publicKey != null) { - return this.publicKey; + const inMemoryPublicKey = await this.stateService.getPublicKey(); + if (inMemoryPublicKey != null) { + return inMemoryPublicKey; } const privateKey = await this.getPrivateKey(); @@ -217,22 +189,25 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - this.publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); - return this.publicKey; + const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); + await this.stateService.setPublicKey(publicKey); + return publicKey; } async getPrivateKey(): Promise { - if (this.privateKey != null) { - return this.privateKey; + const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey(); + if (decryptedPrivateKey != null) { + return decryptedPrivateKey; } - const encPrivateKey = await this.storageService.get(Keys.encPrivateKey); + const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); if (encPrivateKey == null) { return null; } - this.privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null); - return this.privateKey; + const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null); + await this.stateService.setDecryptedPrivateKey(privateKey); + return privateKey; } async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise { @@ -249,16 +224,17 @@ export class CryptoService implements CryptoServiceAbstraction { @sequentialize(() => 'getOrgKeys') async getOrgKeys(): Promise> { - if (this.orgKeys != null && this.orgKeys.size > 0) { - return this.orgKeys; + const orgKeys: Map = new Map(); + const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys(); + if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) { + return decryptedOrganizationKeys; } - const encOrgKeys = await this.storageService.get(Keys.encOrgKeys); + const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys(); if (encOrgKeys == null) { return null; } - const orgKeys: Map = new Map(); let setKey = false; for (const orgId in encOrgKeys) { @@ -272,10 +248,10 @@ export class CryptoService implements CryptoServiceAbstraction { } if (setKey) { - this.orgKeys = orgKeys; + await this.stateService.setDecryptedOrganizationKeys(orgKeys); } - return this.orgKeys; + return orgKeys; } async getOrgKey(orgId: string): Promise { @@ -293,16 +269,17 @@ export class CryptoService implements CryptoServiceAbstraction { @sequentialize(() => 'getProviderKeys') async getProviderKeys(): Promise> { - if (this.providerKeys != null && this.providerKeys.size > 0) { - return this.providerKeys; + const providerKeys: Map = new Map(); + const decryptedProviderKeys = await this.stateService.getDecryptedProviderKeys(); + if (decryptedProviderKeys != null && decryptedProviderKeys.size > 0) { + return decryptedProviderKeys; } - const encProviderKeys = await this.storageService.get(Keys.encProviderKeys); + const encProviderKeys = await this.stateService.getEncryptedProviderKeys(); if (encProviderKeys == null) { return null; } - const providerKeys: Map = new Map(); let setKey = false; for (const orgId in encProviderKeys) { @@ -316,10 +293,10 @@ export class CryptoService implements CryptoServiceAbstraction { } if (setKey) { - this.providerKeys = providerKeys; + await this.stateService.setDecryptedProviderKeys(providerKeys); } - return this.providerKeys; + return providerKeys; } async getProviderKey(providerId: string): Promise { @@ -336,84 +313,87 @@ export class CryptoService implements CryptoServiceAbstraction { } async hasKey(): Promise { - return this.hasKeyInMemory() || await this.hasKeyStored('auto') || await this.hasKeyStored('biometric'); + return await this.hasKeyInMemory() || await this.hasKeyStored(KeySuffixOptions.Auto) || await this.hasKeyStored(KeySuffixOptions.Biometric); } - hasKeyInMemory(): boolean { - return this.key != null; + async hasKeyInMemory(userId?: string): Promise { + return await this.stateService.getCryptoMasterKey({ userId: userId }) != null; } - hasKeyStored(keySuffix: KeySuffixOptions): Promise { - return this.secureStorageService.has(Keys.key, { keySuffix: keySuffix }); + async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise { + const key = keySuffix === KeySuffixOptions.Auto ? + await this.stateService.getCryptoMasterKeyAuto({ userId: userId }) : + await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId }); + + return key != null; } async hasEncKey(): Promise { - const encKey = await this.storageService.get(Keys.encKey); - return encKey != null; + return await this.stateService.getEncryptedCryptoSymmetricKey() != null; } - async clearKey(clearSecretStorage: boolean = true): Promise { - this.key = this.legacyEtmKey = null; + async clearKey(clearSecretStorage: boolean = true, userId?: string): Promise { + await this.stateService.setCryptoMasterKey(null, { userId: userId }); + await this.stateService.setLegacyEtmKey(null, { userId: userId }); if (clearSecretStorage) { - this.clearStoredKey('auto'); - this.clearStoredKey('biometric'); + await this.clearSecretKeyStore(userId); } } async clearStoredKey(keySuffix: KeySuffixOptions) { - await this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix }); + keySuffix === KeySuffixOptions.Auto ? + await this.stateService.setCryptoMasterKeyAuto(null) : + await this.stateService.setCryptoMasterKeyBiometric(null); } - clearKeyHash(): Promise { - this.keyHash = null; - return this.storageService.remove(Keys.keyHash); + async clearKeyHash(userId?: string): Promise { + return await this.stateService.setKeyHash(null, { userId: userId }); } - clearEncKey(memoryOnly?: boolean): Promise { - this.encKey = null; - if (memoryOnly) { - return Promise.resolve(); + async clearEncKey(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId }); } - return this.storageService.remove(Keys.encKey); } - clearKeyPair(memoryOnly?: boolean): Promise { - this.privateKey = null; - this.publicKey = null; - if (memoryOnly) { - return Promise.resolve(); + async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise { + const keysToClear: Promise[] = [ + this.stateService.setDecryptedPrivateKey(null, { userId: userId }), + this.stateService.setPublicKey(null, { userId: userId }), + ]; + if (!memoryOnly) { + keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId })); } - return this.storageService.remove(Keys.encPrivateKey); + return Promise.all(keysToClear); } - clearOrgKeys(memoryOnly?: boolean): Promise { - this.orgKeys = null; - if (memoryOnly) { - return Promise.resolve(); + async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId }); + } + } + + async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedProviderKeys(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedProviderKeys(null, { userId: userId }); } - return this.storageService.remove(Keys.encOrgKeys); } - clearProviderKeys(memoryOnly?: boolean): Promise { - this.providerKeys = null; - if (memoryOnly) { - return Promise.resolve(); - } - return this.storageService.remove(Keys.encOrgKeys); + async clearPinProtectedKey(userId?: string): Promise { + return await this.stateService.setEncryptedPinProtected(null, { userId: userId }); } - clearPinProtectedKey(): Promise { - return this.storageService.remove(ConstantsService.pinProtectedKey); - } - - async clearKeys(): Promise { - await this.clearKey(); - await this.clearKeyHash(); - await this.clearOrgKeys(); - await this.clearProviderKeys(); - await this.clearEncKey(); - await this.clearKeyPair(); - await this.clearPinProtectedKey(); + async clearKeys(userId?: string): Promise { + await this.clearKey(true, userId); + await this.clearKeyHash(userId); + await this.clearOrgKeys(false, userId); + await this.clearProviderKeys(false, userId); + await this.clearEncKey(false, userId); + await this.clearKeyPair(false, userId); + await this.clearPinProtectedKey(userId); } async toggleKey(): Promise { @@ -442,7 +422,7 @@ export class CryptoService implements CryptoServiceAbstraction { protectedKeyCs: EncString = null): Promise { if (protectedKeyCs == null) { - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + const pinProtectedKey = await this.stateService.getEncryptedPinProtected(); if (pinProtectedKey == null) { throw new Error('No PIN protected key found.'); } @@ -699,7 +679,7 @@ export class CryptoService implements CryptoServiceAbstraction { async validateKey(key: SymmetricCryptoKey) { try { - const encPrivateKey = await this.storageService.get(Keys.encPrivateKey); + const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); const encKey = await this.getEncKey(key); if (encPrivateKey == null || encKey == null) { return false; @@ -715,29 +695,30 @@ export class CryptoService implements CryptoServiceAbstraction { } // Helpers - - protected async storeKey(key: SymmetricCryptoKey) { - if (await this.shouldStoreKey('auto') || await this.shouldStoreKey('biometric')) { - this.secureStorageService.save(Keys.key, key.keyB64); + protected async storeKey(key: SymmetricCryptoKey, userId?: string) { + if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId) || await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) { + await this.stateService.setCryptoMasterKeyB64(key.keyB64, { userId: userId }); } else { - this.secureStorageService.remove(Keys.key); + await this.stateService.setCryptoMasterKeyB64(null, { userId: userId }); } } - protected async shouldStoreKey(keySuffix: KeySuffixOptions) { + protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) { let shouldStoreKey = false; - if (keySuffix === 'auto') { - const vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + if (keySuffix === KeySuffixOptions.Auto) { + const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); shouldStoreKey = vaultTimeout == null; - } else if (keySuffix === 'biometric') { - const biometricUnlock = await this.storageService.get(ConstantsService.biometricUnlockKey); + } else if (keySuffix === KeySuffixOptions.Biometric) { + const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId }); shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage(); } return shouldStoreKey; } - protected retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { - return this.secureStorageService.get(Keys.key, { keySuffix: keySuffix }); + protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) { + return keySuffix === KeySuffixOptions.Auto ? + await this.stateService.getCryptoMasterKeyAuto({ userId: userId }) : + await this.stateService.getCryptoMasterKeyBiometric({ userId: userId }); } private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { @@ -759,7 +740,7 @@ export class CryptoService implements CryptoServiceAbstraction { private async aesDecryptToUtf8(encType: EncryptionType, data: string, iv: string, mac: string, key: SymmetricCryptoKey): Promise { const keyForEnc = await this.getKeyForEncryption(key); - const theKey = this.resolveLegacyKey(encType, keyForEnc); + const theKey = await this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && mac == null) { this.logService.error('mac required.'); @@ -788,7 +769,7 @@ export class CryptoService implements CryptoServiceAbstraction { private async aesDecryptToBytes(encType: EncryptionType, data: ArrayBuffer, iv: ArrayBuffer, mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { const keyForEnc = await this.getKeyForEncryption(key); - const theKey = this.resolveLegacyKey(encType, keyForEnc); + const theKey = await this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && mac == null) { return null; @@ -830,14 +811,16 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.getKey(); } - private resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): SymmetricCryptoKey { + private async resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): Promise { if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && key.encType === EncryptionType.AesCbc256_B64) { // Old encrypt-then-mac scheme, make a new key - if (this.legacyEtmKey == null) { - this.legacyEtmKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); + let legacyKey = await this.stateService.getLegacyEtmKey(); + if (legacyKey == null) { + legacyKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); + await this.stateService.setLegacyEtmKey(legacyKey); } - return this.legacyEtmKey; + return legacyKey; } return key; @@ -885,4 +868,9 @@ export class CryptoService implements CryptoServiceAbstraction { } return [new SymmetricCryptoKey(encKey), encKeyEnc]; } + + private async clearSecretKeyStore(userId?: string): Promise { + await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); + } } diff --git a/common/src/services/environment.service.ts b/common/src/services/environment.service.ts index 8d10995a00..0cbe1a7d54 100644 --- a/common/src/services/environment.service.ts +++ b/common/src/services/environment.service.ts @@ -2,10 +2,8 @@ import { Observable, Subject } from 'rxjs'; import { EnvironmentUrls } from '../models/domain/environmentUrls'; -import { ConstantsService } from './constants.service'; - import { EnvironmentService as EnvironmentServiceAbstraction, Urls } from '../abstractions/environment.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; export class EnvironmentService implements EnvironmentServiceAbstraction { @@ -21,7 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { private eventsUrl: string; private keyConnectorUrl: string; - constructor(private storageService: StorageService) {} + constructor(private stateService: StateService) {} hasBaseUrl() { return this.baseUrl != null; @@ -109,7 +107,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } async setUrlsFromStorage(): Promise { - const urlsObj: any = await this.storageService.get(ConstantsService.environmentUrlsKey); + const urlsObj: any = await this.stateService.getEnvironmentUrls(); const urls = urlsObj || { base: null, api: null, @@ -148,7 +146,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.keyConnector = this.formatUrl(urls.keyConnector); if (saveSettings) { - await this.storageService.save(ConstantsService.environmentUrlsKey, { + await this.stateService.setEnvironmentUrls({ base: urls.base, api: urls.api, identity: urls.identity, diff --git a/common/src/services/event.service.ts b/common/src/services/event.service.ts index 452133fa6b..7f1de12ab5 100644 --- a/common/src/services/event.service.ts +++ b/common/src/services/event.service.ts @@ -7,18 +7,16 @@ import { EventRequest } from '../models/request/eventRequest'; import { ApiService } from '../abstractions/api.service'; import { CipherService } from '../abstractions/cipher.service'; import { EventService as EventServiceAbstraction } from '../abstractions/event.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; - import { LogService } from '../abstractions/log.service'; -import { ConstantsService } from './constants.service'; +import { OrganizationService } from '../abstractions/organization.service'; +import { StateService } from '../abstractions/state.service'; export class EventService implements EventServiceAbstraction { private inited = false; - constructor(private storageService: StorageService, private apiService: ApiService, - private userService: UserService, private cipherService: CipherService, - private logService: LogService) { } + constructor(private apiService: ApiService, private cipherService: CipherService, + private stateService: StateService, private logService: LogService, + private organizationService: OrganizationService) { } init(checkOnInterval: boolean) { if (this.inited) { @@ -33,11 +31,11 @@ export class EventService implements EventServiceAbstraction { } async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { - const authed = await this.userService.isAuthenticated(); + const authed = await this.stateService.getIsAuthenticated(); if (!authed) { return; } - const organizations = await this.userService.getAllOrganizations(); + const organizations = await this.organizationService.getAll(); if (organizations == null) { return; } @@ -51,7 +49,7 @@ export class EventService implements EventServiceAbstraction { return; } } - let eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); + let eventCollection = await this.stateService.getEventCollection(); if (eventCollection == null) { eventCollection = []; } @@ -60,18 +58,18 @@ export class EventService implements EventServiceAbstraction { event.cipherId = cipherId; event.date = new Date().toISOString(); eventCollection.push(event); - await this.storageService.save(ConstantsService.eventCollectionKey, eventCollection); + await this.stateService.setEventCollection(eventCollection); if (uploadImmediately) { await this.uploadEvents(); } } - async uploadEvents(): Promise { - const authed = await this.userService.isAuthenticated(); + async uploadEvents(userId?: string): Promise { + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; } - const eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); + const eventCollection = await this.stateService.getEventCollection({ userId: userId }); if (eventCollection == null || eventCollection.length === 0) { return; } @@ -84,13 +82,13 @@ export class EventService implements EventServiceAbstraction { }); try { await this.apiService.postEventsCollect(request); - this.clearEvents(); + this.clearEvents(userId); } catch (e) { this.logService.error(e); } } - async clearEvents(): Promise { - await this.storageService.remove(ConstantsService.eventCollectionKey); + async clearEvents(userId?: string): Promise { + await this.stateService.setEventCollection(null, { userId: userId }); } } diff --git a/common/src/services/folder.service.ts b/common/src/services/folder.service.ts index 0f3ae50e23..d8b9543081 100644 --- a/common/src/services/folder.service.ts +++ b/common/src/services/folder.service.ts @@ -15,28 +15,22 @@ import { CipherService } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { StateService } from '../abstractions/state.service'; + import { CipherData } from '../models/data/cipherData'; import { ServiceUtils } from '../misc/serviceUtils'; import { Utils } from '../misc/utils'; -const Keys = { - foldersPrefix: 'folders_', - ciphersPrefix: 'ciphers_', -}; const NestingDelimiter = '/'; export class FolderService implements FolderServiceAbstraction { - decryptedFolderCache: FolderView[]; + constructor(private cryptoService: CryptoService, private apiService: ApiService, + private i18nService: I18nService, private cipherService: CipherService, + private stateService: StateService) { } - constructor(private cryptoService: CryptoService, private userService: UserService, - private apiService: ApiService, private storageService: StorageService, - private i18nService: I18nService, private cipherService: CipherService) { } - - clearCache(): void { - this.decryptedFolderCache = null; + async clearCache(userId?: string): Promise { + await this.stateService.setDecryptedFolders(null, { userId: userId }); } async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise { @@ -47,9 +41,7 @@ export class FolderService implements FolderServiceAbstraction { } async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); + const folders = await this.stateService.getEncryptedFolders(); if (folders == null || !folders.hasOwnProperty(id)) { return null; } @@ -58,9 +50,7 @@ export class FolderService implements FolderServiceAbstraction { } async getAll(): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); + const folders = await this.stateService.getEncryptedFolders(); const response: Folder[] = []; for (const id in folders) { if (folders.hasOwnProperty(id)) { @@ -71,8 +61,9 @@ export class FolderService implements FolderServiceAbstraction { } async getAllDecrypted(): Promise { - if (this.decryptedFolderCache != null) { - return this.decryptedFolderCache; + const decryptedFolders = await this.stateService.getDecryptedFolders(); + if (decryptedFolders != null) { + return decryptedFolders; } const hasKey = await this.cryptoService.hasKey(); @@ -94,8 +85,8 @@ export class FolderService implements FolderServiceAbstraction { noneFolder.name = this.i18nService.t('noneFolder'); decFolders.push(noneFolder); - this.decryptedFolderCache = decFolders; - return this.decryptedFolderCache; + await this.stateService.setDecryptedFolders(decFolders); + return decFolders; } async getAllNested(): Promise[]> { @@ -127,15 +118,13 @@ export class FolderService implements FolderServiceAbstraction { response = await this.apiService.putFolder(folder.id, request); } - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); const data = new FolderData(response, userId); await this.upsert(data); } async upsert(folder: FolderData | FolderData[]): Promise { - const userId = await this.userService.getUserId(); - let folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); + let folders = await this.stateService.getEncryptedFolders(); if (folders == null) { folders = {}; } @@ -149,25 +138,22 @@ export class FolderService implements FolderServiceAbstraction { }); } - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); } async replace(folders: { [id: string]: FolderData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.foldersPrefix + userId); - this.decryptedFolderCache = null; + async clear(userId?: string): Promise { + await this.stateService.setDecryptedFolders(null, { userId: userId }); + await this.stateService.setEncryptedFolders(null, { userId: userId }); } async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); + const folders = await this.stateService.getEncryptedFolders(); if (folders == null) { return; } @@ -183,11 +169,11 @@ export class FolderService implements FolderServiceAbstraction { }); } - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); // Items in a deleted folder are re-assigned to "No Folder" - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>(Keys.ciphersPrefix + userId); + const ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers != null) { const updates: CipherData[] = []; for (const cId in ciphers) { diff --git a/common/src/services/keyConnector.service.ts b/common/src/services/keyConnector.service.ts index d597469f39..1e6894f222 100644 --- a/common/src/services/keyConnector.service.ts +++ b/common/src/services/keyConnector.service.ts @@ -2,9 +2,9 @@ import { ApiService } from '../abstractions/api.service'; import { CryptoService } from '../abstractions/crypto.service'; import { KeyConnectorService as KeyConnectorServiceAbstraction } from '../abstractions/keyConnector.service'; import { LogService } from '../abstractions/log.service'; -import { StorageService } from '../abstractions/storage.service'; +import { OrganizationService } from '../abstractions/organization.service'; +import { StateService } from '../abstractions/state.service'; import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; import { OrganizationUserType } from '../enums/organizationUserType'; @@ -14,25 +14,17 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; -const Keys = { - usesKeyConnector: 'usesKeyConnector', - convertAccountToKeyConnector: 'convertAccountToKeyConnector', -}; - export class KeyConnectorService implements KeyConnectorServiceAbstraction { - private usesKeyConnector?: boolean = null; - - constructor(private storageService: StorageService, private userService: UserService, - private cryptoService: CryptoService, private apiService: ApiService, - private tokenService: TokenService, private logService: LogService) { } + constructor(private stateService: StateService, private cryptoService: CryptoService, + private apiService: ApiService, private tokenService: TokenService, + private logService: LogService, private organizationService: OrganizationService) { } setUsesKeyConnector(usesKeyConnector: boolean) { - this.usesKeyConnector = usesKeyConnector; - return this.storageService.save(Keys.usesKeyConnector, usesKeyConnector); + return this.stateService.setUsesKeyConnector(usesKeyConnector); } async getUsesKeyConnector(): Promise { - return this.usesKeyConnector ??= await this.storageService.get(Keys.usesKeyConnector); + return await this.stateService.getUsesKeyConnector(); } async userNeedsMigration() { @@ -70,7 +62,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async getManagingOrganization() { - const orgs = await this.userService.getAllOrganizations(); + const orgs = await this.organizationService.getAll(); return orgs.find(o => o.keyConnectorEnabled && o.type !== OrganizationUserType.Admin && @@ -79,15 +71,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async setConvertAccountRequired(status: boolean) { - await this.storageService.save(Keys.convertAccountToKeyConnector, status); + await this.stateService.setConvertAccountToKeyConnector(status); } async getConvertAccountRequired(): Promise { - return await this.storageService.get(Keys.convertAccountToKeyConnector); + return await this.stateService.getConvertAccountToKeyConnector(); } async removeConvertAccountRequired() { - await this.storageService.remove(Keys.convertAccountToKeyConnector); + await this.stateService.setConvertAccountToKeyConnector(null); } async clear() { diff --git a/common/src/services/notifications.service.ts b/common/src/services/notifications.service.ts index b1d5723b2d..b10b89bb81 100644 --- a/common/src/services/notifications.service.ts +++ b/common/src/services/notifications.service.ts @@ -8,8 +8,8 @@ import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; import { LogService } from '../abstractions/log.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; +import { StateService } from '../abstractions/state.service'; import { SyncService } from '../abstractions/sync.service'; -import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; import { @@ -27,10 +27,10 @@ export class NotificationsService implements NotificationsServiceAbstraction { private inactive = false; private reconnectTimer: any = null; - constructor(private userService: UserService, private syncService: SyncService, - private appIdService: AppIdService, private apiService: ApiService, - private vaultTimeoutService: VaultTimeoutService, private environmentService: EnvironmentService, - private logoutCallback: () => Promise, private logService: LogService) { + constructor(private syncService: SyncService, private appIdService: AppIdService, + private apiService: ApiService, private vaultTimeoutService: VaultTimeoutService, + private environmentService: EnvironmentService, private logoutCallback: () => Promise, + private logService: LogService, private stateService: StateService) { this.environmentService.urls.subscribe(() => { if (!this.inited) { return; @@ -117,9 +117,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { return; } - const isAuthenticated = await this.userService.isAuthenticated(); + const isAuthenticated = await this.stateService.getIsAuthenticated(); const payloadUserId = notification.payload.userId || notification.payload.UserId; - const myUserId = await this.userService.getUserId(); + const myUserId = await this.stateService.getUserId(); if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { return; } @@ -202,7 +202,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async isAuthedAndUnlocked() { - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { const locked = await this.vaultTimeoutService.isLocked(); return !locked; } diff --git a/common/src/services/organization.service.ts b/common/src/services/organization.service.ts new file mode 100644 index 0000000000..cdfe2981f3 --- /dev/null +++ b/common/src/services/organization.service.ts @@ -0,0 +1,49 @@ +import { OrganizationService as OrganizationServiceAbstraction } from '../abstractions/organization.service'; +import { StateService } from '../abstractions/state.service'; + +import { OrganizationData } from '../models/data/organizationData'; + +import { Organization } from '../models/domain/organization'; + +export class OrganizationService implements OrganizationServiceAbstraction { + constructor(private stateService: StateService) { + } + + async get(id: string): Promise { + const organizations = await this.stateService.getOrganizations(); + if (organizations == null || !organizations.hasOwnProperty(id)) { + return null; + } + + return new Organization(organizations[id]); + } + + async getByIdentifier(identifier: string): Promise { + const organizations = await this.getAll(); + if (organizations == null || organizations.length === 0) { + return null; + } + + return organizations.find(o => o.identifier === identifier); + } + + async getAll(userId?: string): Promise { + const organizations = await this.stateService.getOrganizations({ userId: userId }); + const response: Organization[] = []; + for (const id in organizations) { + if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { + response.push(new Organization(organizations[id])); + } + } + return response; + } + + async save(organizations: {[id: string]: OrganizationData}) { + return await this.stateService.setOrganizations(organizations); + } + + async canManageSponsorships(): Promise { + const orgs = await this.getAll(); + return orgs.some(o => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null); + } +} diff --git a/common/src/services/passwordGeneration.service.ts b/common/src/services/passwordGeneration.service.ts index ab4af5b950..c6d0d7cc68 100644 --- a/common/src/services/passwordGeneration.service.ts +++ b/common/src/services/passwordGeneration.service.ts @@ -10,7 +10,7 @@ import { PasswordGenerationService as PasswordGenerationServiceAbstraction, } from '../abstractions/passwordGeneration.service'; import { PolicyService } from '../abstractions/policy.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { EEFLongWordList } from '../misc/wordlist'; @@ -34,19 +34,11 @@ const DefaultOptions = { includeNumber: false, }; -const Keys = { - options: 'passwordGenerationOptions', - history: 'generatedPasswordHistory', -}; - const MaxPasswordsInHistory = 100; export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { - private optionsCache: any; - private history: GeneratedPasswordHistory[]; - - constructor(private cryptoService: CryptoService, private storageService: StorageService, - private policyService: PolicyService) { } + constructor(private cryptoService: CryptoService, private policyService: PolicyService, + private stateService: StateService) { } async generatePassword(options: any): Promise { // overload defaults with given options @@ -188,17 +180,16 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { - if (this.optionsCache == null) { - const options = await this.storageService.get(Keys.options); - if (options == null) { - this.optionsCache = DefaultOptions; - } else { - this.optionsCache = Object.assign({}, DefaultOptions, options); - } + let options = await this.stateService.getPasswordGenerationOptions(); + if (options == null) { + options = DefaultOptions; + } else { + options = Object.assign({}, DefaultOptions, options); } - const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(this.optionsCache); - this.optionsCache = enforcedOptions[0]; - return [this.optionsCache, enforcedOptions[1]]; + await this.stateService.setPasswordGenerationOptions(options); + const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options); + options = enforcedOptions[0]; + return [options, enforcedOptions[1]]; } async enforcePasswordGeneratorPoliciesOnOptions(options: any): Promise<[any, PasswordGeneratorPolicyOptions]> { @@ -332,8 +323,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } async saveOptions(options: any) { - await this.storageService.save(Keys.options, options); - this.optionsCache = options; + await this.stateService.setPasswordGenerationOptions(options); } async getHistory(): Promise { @@ -342,12 +332,16 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return new Array(); } - if (!this.history) { - const encrypted = await this.storageService.get(Keys.history); - this.history = await this.decryptHistory(encrypted); + if (await this.stateService.getDecryptedPasswordGenerationHistory() != null) { + const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory(); + const decrypted = await this.decryptHistory(encrypted); + await this.stateService.setDecryptedPasswordGenerationHistory(decrypted); } - return this.history || new Array(); + const passwordGenerationHistory = await this.stateService.getDecryptedPasswordGenerationHistory(); + return passwordGenerationHistory != null ? + passwordGenerationHistory : + new Array(); } async addHistory(password: string): Promise { @@ -372,12 +366,12 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } const newHistory = await this.encryptHistory(currentHistory); - return await this.storageService.save(Keys.history, newHistory); + return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory); } - async clear(): Promise { - this.history = []; - return await this.storageService.remove(Keys.history); + async clear(userId?: string): Promise { + await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId }); + await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); } passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult { diff --git a/common/src/services/policy.service.ts b/common/src/services/policy.service.ts index 4d23448af8..042d01b0c6 100644 --- a/common/src/services/policy.service.ts +++ b/common/src/services/policy.service.ts @@ -1,6 +1,6 @@ +import { OrganizationService } from '../abstractions/organization.service'; import { PolicyService as PolicyServiceAbstraction } from '../abstractions/policy.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { StateService } from '../abstractions/state.service'; import { PolicyData } from '../models/data/policyData'; @@ -17,43 +17,40 @@ import { ApiService } from '../abstractions/api.service'; import { ListResponse } from '../models/response/listResponse'; import { PolicyResponse } from '../models/response/policyResponse'; -const Keys = { - policiesPrefix: 'policies_', -}; - export class PolicyService implements PolicyServiceAbstraction { policyCache: Policy[]; - constructor(private userService: UserService, private storageService: StorageService, + constructor(private stateService: StateService, private organizationService: OrganizationService, private apiService: ApiService) { } - clearCache(): void { - this.policyCache = null; + async clearCache(): Promise { + await this.stateService.setDecryptedPolicies(null); } - async getAll(type?: PolicyType): Promise { - if (this.policyCache == null) { - const userId = await this.userService.getUserId(); - const policies = await this.storageService.get<{ [id: string]: PolicyData; }>( - Keys.policiesPrefix + userId); - const response: Policy[] = []; - for (const id in policies) { - if (policies.hasOwnProperty(id)) { - response.push(new Policy(policies[id])); + async getAll(type?: PolicyType, userId?: string): Promise { + let response: Policy[] = []; + const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId }); + if (decryptedPolicies != null) { + response = decryptedPolicies; + } else { + const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId }); + for (const id in diskPolicies) { + if (diskPolicies.hasOwnProperty(id)) { + response.push(new Policy(diskPolicies[id])); } } - this.policyCache = response; + await this.stateService.setDecryptedPolicies(response, { userId: userId }); } if (type != null) { - return this.policyCache.filter(p => p.type === type); + return response.filter(policy => policy.type === type); } else { - return this.policyCache; + return response; } } async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise { - const org = await this.userService.getOrganization(organizationId); + const org = await this.organizationService.get(organizationId); if (org?.isProviderUser) { const orgPolicies = await this.apiService.getPolicies(organizationId); const policy = orgPolicies.data.find(p => p.organizationId === organizationId); @@ -70,14 +67,13 @@ export class PolicyService implements PolicyServiceAbstraction { } async replace(policies: { [id: string]: PolicyData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.policiesPrefix + userId, policies); - this.policyCache = null; + await this.stateService.setDecryptedPolicies(null); + await this.stateService.setEncryptedPolicies(policies); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.policiesPrefix + userId); - this.policyCache = null; + async clear(userId?: string): Promise { + await this.stateService.setDecryptedPolicies(null, { userId: userId }); + await this.stateService.setEncryptedPolicies(null, { userId: userId }); } async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise { @@ -187,9 +183,9 @@ export class PolicyService implements PolicyServiceAbstraction { return policiesData.map(p => new Policy(p)); } - async policyAppliesToUser(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) { - const policies = await this.getAll(policyType); - const organizations = await this.userService.getAllOrganizations(); + async policyAppliesToUser(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean, userId?: string) { + const policies = await this.getAll(policyType, userId); + const organizations = await this.organizationService.getAll(userId); let filteredPolicies; if (policyFilter != null) { diff --git a/common/src/services/provider.service.ts b/common/src/services/provider.service.ts new file mode 100644 index 0000000000..9e1f560245 --- /dev/null +++ b/common/src/services/provider.service.ts @@ -0,0 +1,35 @@ +import { ProviderService as ProviderServiceAbstraction } from '../abstractions/provider.service'; +import { StateService } from '../abstractions/state.service'; + +import { ProviderData } from '../models/data/providerData'; + +import { Provider } from '../models/domain/provider'; + +export class ProviderService implements ProviderServiceAbstraction { + constructor(private stateService: StateService) { + } + + async get(id: string): Promise { + const providers = await this.stateService.getProviders(); + if (providers == null || !providers.hasOwnProperty(id)) { + return null; + } + + return new Provider(providers[id]); + } + + async getAll(): Promise { + const providers = await this.stateService.getProviders(); + const response: Provider[] = []; + for (const id in providers) { + if (providers.hasOwnProperty(id)) { + response.push(new Provider(providers[id])); + } + } + return response; + } + + async save(providers: { [id: string]: ProviderData; }) { + await this.stateService.setProviders(providers); + } +} diff --git a/common/src/services/send.service.ts b/common/src/services/send.service.ts index a5a9a69994..ff0d09ae11 100644 --- a/common/src/services/send.service.ts +++ b/common/src/services/send.service.ts @@ -12,7 +12,6 @@ import { SendFile } from '../models/domain/sendFile'; import { SendText } from '../models/domain/sendText'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { FileUploadType } from '../enums/fileUploadType'; import { SendType } from '../enums/sendType'; import { SendView } from '../models/view/sendView'; @@ -23,25 +22,17 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { FileUploadService } from '../abstractions/fileUpload.service'; import { I18nService } from '../abstractions/i18n.service'; import { SendService as SendServiceAbstraction } from '../abstractions/send.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { StateService } from '../abstractions/state.service'; import { Utils } from '../misc/utils'; -const Keys = { - sendsPrefix: 'sends_', -}; - export class SendService implements SendServiceAbstraction { - decryptedSendCache: SendView[]; + constructor(private cryptoService: CryptoService, private apiService: ApiService, + private fileUploadService: FileUploadService, private i18nService: I18nService, + private cryptoFunctionService: CryptoFunctionService, private stateService: StateService) { } - constructor(private cryptoService: CryptoService, private userService: UserService, - private apiService: ApiService, private fileUploadService: FileUploadService, - private storageService: StorageService, private i18nService: I18nService, - private cryptoFunctionService: CryptoFunctionService) { } - - clearCache(): void { - this.decryptedSendCache = null; + async clearCache(): Promise { + await this.stateService.setDecryptedSends(null); } async encrypt(model: SendView, file: File | ArrayBuffer, password: string, @@ -85,9 +76,7 @@ export class SendService implements SendServiceAbstraction { } async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); + const sends = await this.stateService.getEncryptedSends(); if (sends == null || !sends.hasOwnProperty(id)) { return null; } @@ -96,9 +85,7 @@ export class SendService implements SendServiceAbstraction { } async getAll(): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); + const sends = await this.stateService.getEncryptedSends(); const response: Send[] = []; for (const id in sends) { if (sends.hasOwnProperty(id)) { @@ -109,16 +96,17 @@ export class SendService implements SendServiceAbstraction { } async getAllDecrypted(): Promise { - if (this.decryptedSendCache != null) { - return this.decryptedSendCache; + let decSends = await this.stateService.getDecryptedSends(); + if (decSends != null) { + return decSends; } + decSends = []; const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { throw new Error('No key.'); } - const decSends: SendView[] = []; const promises: Promise[] = []; const sends = await this.getAll(); sends.forEach(send => { @@ -128,8 +116,8 @@ export class SendService implements SendServiceAbstraction { await Promise.all(promises); decSends.sort(Utils.getSortFunction(this.i18nService, 'name')); - this.decryptedSendCache = decSends; - return this.decryptedSendCache; + await this.stateService.setDecryptedSends(decSends); + return decSends; } async saveWithServer(sendData: [Send, EncArrayBuffer]): Promise { @@ -160,7 +148,7 @@ export class SendService implements SendServiceAbstraction { response = await this.apiService.putSend(sendData[0].id, request); } - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); const data = new SendData(response, userId); await this.upsert(data); } @@ -191,9 +179,7 @@ export class SendService implements SendServiceAbstraction { } async upsert(send: SendData | SendData[]): Promise { - const userId = await this.userService.getUserId(); - let sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); + let sends = await this.stateService.getEncryptedSends(); if (sends == null) { sends = {}; } @@ -207,25 +193,21 @@ export class SendService implements SendServiceAbstraction { }); } - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; + await this.replace(sends); } async replace(sends: { [id: string]: SendData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; + await this.stateService.setDecryptedSends(null); + await this.stateService.setEncryptedSends(sends); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.sendsPrefix + userId); - this.decryptedSendCache = null; + async clear(): Promise { + await this.stateService.setDecryptedSends(null); + await this.stateService.setEncryptedSends(null); } async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); + const sends = await this.stateService.getEncryptedSends(); if (sends == null) { return; } @@ -241,8 +223,7 @@ export class SendService implements SendServiceAbstraction { }); } - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; + await this.replace(sends); } async deleteWithServer(id: string): Promise { @@ -252,7 +233,7 @@ export class SendService implements SendServiceAbstraction { async removePasswordWithServer(id: string): Promise { const response = await this.apiService.putSendRemovePassword(id); - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); const data = new SendData(response, userId); await this.upsert(data); } @@ -270,7 +251,7 @@ export class SendService implements SendServiceAbstraction { reject(e); } }; - reader.onerror = evt => { + reader.onerror = () => { reject('Error reading file.'); }; }); diff --git a/common/src/services/settings.service.ts b/common/src/services/settings.service.ts index 51d970f3c1..a8c7ed5ba5 100644 --- a/common/src/services/settings.service.ts +++ b/common/src/services/settings.service.ts @@ -1,6 +1,5 @@ import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { StateService } from '../abstractions/state.service'; const Keys = { settingsPrefix: 'settings_', @@ -8,13 +7,11 @@ const Keys = { }; export class SettingsService implements SettingsServiceAbstraction { - private settingsCache: any; - - constructor(private userService: UserService, private storageService: StorageService) { + constructor(private stateService: StateService) { } - clearCache(): void { - this.settingsCache = null; + async clearCache(): Promise { + await this.stateService.setSettings(null); } getEquivalentDomains(): Promise { @@ -25,19 +22,18 @@ export class SettingsService implements SettingsServiceAbstraction { await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.settingsPrefix + userId); - this.clearCache(); + async clear(userId?: string): Promise { + await this.stateService.setSettings(null, { userId: userId }); } // Helpers private async getSettings(): Promise { - if (this.settingsCache == null) { - const userId = await this.userService.getUserId(); - this.settingsCache = this.storageService.get(Keys.settingsPrefix + userId); + const settings = await this.stateService.getSettings(); + if (settings == null) { + const userId = await this.stateService.getUserId(); } - return this.settingsCache; + return settings; } private async getSettingsKey(key: string): Promise { @@ -49,14 +45,12 @@ export class SettingsService implements SettingsServiceAbstraction { } private async setSettingsKey(key: string, value: any): Promise { - const userId = await this.userService.getUserId(); let settings = await this.getSettings(); if (!settings) { settings = {}; } settings[key] = value; - await this.storageService.save(Keys.settingsPrefix + userId, settings); - this.settingsCache = settings; + await this.stateService.setSettings(settings); } } diff --git a/common/src/services/state.service.ts b/common/src/services/state.service.ts index 03a09e30fc..6a6c71ec2a 100644 --- a/common/src/services/state.service.ts +++ b/common/src/services/state.service.ts @@ -1,27 +1,1579 @@ import { StateService as StateServiceAbstraction } from '../abstractions/state.service'; +import { Account } from '../models/domain/account'; + +import { LogService } from '../abstractions/log.service'; +import { StorageService } from '../abstractions/storage.service'; + +import { HtmlStorageLocation } from '../enums/htmlStorageLocation'; +import { KdfType } from '../enums/kdfType'; +import { StorageLocation } from '../enums/storageLocation'; +import { UriMatchType } from '../enums/uriMatchType'; + +import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; +import { FolderView } from '../models/view/folderView'; +import { SendView } from '../models/view/sendView'; + +import { EncString } from '../models/domain/encString'; +import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { GlobalState } from '../models/domain/globalState'; +import { Policy } from '../models/domain/policy'; +import { State } from '../models/domain/state'; +import { StorageOptions } from '../models/domain/storageOptions'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { EventData } from '../models/data/eventData'; +import { FolderData } from '../models/data/folderData'; +import { OrganizationData } from '../models/data/organizationData'; +import { PolicyData } from '../models/data/policyData'; +import { ProviderData } from '../models/data/providerData'; +import { SendData } from '../models/data/sendData'; + +import { BehaviorSubject } from 'rxjs'; + +import { StateMigrationService } from './stateMigration.service'; + export class StateService implements StateServiceAbstraction { - private state: any = {}; + accounts = new BehaviorSubject<{ [userId: string]: Account }>({}); + activeAccount = new BehaviorSubject(null); - get(key: string): Promise { - if (this.state.hasOwnProperty(key)) { - return Promise.resolve(this.state[key]); + private state: State = new State(); + + constructor( + private storageService: StorageService, + private secureStorageService: StorageService, + private logService: LogService, + private stateMigrationService: StateMigrationService + ) { } + + async init(): Promise { + if (await this.stateMigrationService.needsMigration()) { + await this.stateMigrationService.migrate(); + } + if (this.state.activeUserId == null) { + await this.loadStateFromDisk(); } - return Promise.resolve(null); } - save(key: string, obj: any): Promise { - this.state[key] = obj; - return Promise.resolve(); + async loadStateFromDisk() { + if (await this.getActiveUserIdFromStorage() != null) { + const diskState = await this.storageService.get('state', await this.defaultOnDiskOptions()); + this.state = diskState; + await this.pruneInMemoryAccounts(); + await this.saveStateToStorage(this.state, await this.defaultOnDiskMemoryOptions()); + await this.pushAccounts(); + } } - remove(key: string): Promise { - delete this.state[key]; - return Promise.resolve(); + async addAccount(account: Account) { + if (account?.profile?.userId == null) { + return; + } + this.state.accounts[account.profile.userId] = account; + await this.scaffoldNewAccountStorage(account); + await this.setActiveUser(account.profile.userId); + this.activeAccount.next(account.profile.userId); } - purge(): Promise { - this.state = {}; - return Promise.resolve(); + async setActiveUser(userId: string): Promise { + this.state.activeUserId = userId; + const storedState = await this.storageService.get('state', await this.defaultOnDiskOptions()); + storedState.activeUserId = userId; + await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); + await this.pushAccounts(); + this.activeAccount.next(this.state.activeUserId); + } + + async clean(options?: StorageOptions): Promise { + // Find and set the next active user if any exists + if (options?.userId == null || options.userId === await this.getUserId()) { + for (const userId in this.state.accounts) { + if (userId == null) { + continue; + } + if (await this.getIsAuthenticated({ userId: userId })) { + await this.setActiveUser(userId); + break; + } + await this.setActiveUser(null); + } + } + + await this.removeAccountFromSessionStorage(options?.userId); + await this.removeAccountFromLocalStorage(options?.userId); + await this.removeAccountFromSecureStorage(options?.userId); + this.removeAccountFromMemory(options?.userId); + await this.pushAccounts(); + } + + async getAccessToken(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.tokens?.accessToken; + } + + async setAccessToken(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.tokens.accessToken = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getAddEditCipherInfo(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.addEditCipherInfo; + } + + async setAddEditCipherInfo(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.addEditCipherInfo = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getAlwaysShowDock(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.alwaysShowDock ?? false; + } + + async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.alwaysShowDock = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getApiKeyClientId(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.apiKeyClientId; + } + + async setApiKeyClientId(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.apiKeyClientId = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getApiKeyClientSecret(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.apiKeyClientSecret; + } + + async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.apiKeyClientSecret = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.autoConfirmFingerPrints ?? true; + } + + async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.autoConfirmFingerPrints = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getAutoFillOnPageLoadDefault(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.autoFillOnPageLoadDefault ?? false; + } + + async setAutoFillOnPageLoadDefault(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.autoFillOnPageLoadDefault = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getBiometricAwaitingAcceptance(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.biometricAwaitingAcceptance ?? false; + } + + async setBiometricAwaitingAcceptance(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.biometricAwaitingAcceptance = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getBiometricFingerprintValidated(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.biometricFingerprintValidated ?? false; + } + + async setBiometricFingerprintValidated(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.biometricFingerprintValidated = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getBiometricLocked(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.settings?.biometricLocked ?? false; + } + + async setBiometricLocked(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.settings.biometricLocked = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getBiometricText(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.biometricText; + } + + async setBiometricText(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.biometricText = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getBiometricUnlock(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.biometricUnlock ?? false; + } + + async setBiometricUnlock(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.biometricUnlock = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getCanAccessPremium(options?: StorageOptions): Promise { + if (!await this.getIsAuthenticated(options)) { + return false; + } + + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + if (account.profile.hasPremiumPersonally) { + return true; + } + + const organizations = await this.getOrganizations(options); + if (organizations == null) { + return false; + } + + for (const id of Object.keys(organizations)) { + const o = organizations[id]; + if (o.enabled && o.usersGetPremium && !o.isProviderUser) { + return true; + } + } + + return false; + } + + async getClearClipboard(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.clearClipboard; + } + + async setClearClipboard(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.settings.clearClipboard = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getCollapsedGroupings(options?: StorageOptions): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.collapsedGroupings; + } + + async setCollapsedGroupings(value: Set, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.collapsedGroupings = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getConvertAccountToKeyConnector(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.convertAccountToKeyConnector; + } + + async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.convertAccountToKeyConnector = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getCryptoMasterKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.cryptoMasterKey; + } + + async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.keys.cryptoMasterKey = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getCryptoMasterKeyAuto(options?: StorageOptions): Promise { + options = this.reconcileOptions(this.reconcileOptions(options, { keySuffix: 'auto' }), await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return null; + } + return await this.secureStorageService.get(`${options.userId}_masterkey_auto`, options); + } + + async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions(this.reconcileOptions(options, { keySuffix: 'auto' }), await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey_auto`, value, options); + } + + async getCryptoMasterKeyB64(options?: StorageOptions): Promise { + options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return null; + } + return await this.secureStorageService.get(`${options?.userId}_masterkey`, options); + } + + async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey`, value, options); + } + + async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise { + options = this.reconcileOptions(this.reconcileOptions(options, { keySuffix: 'biometric' }), await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + + return null; + } + return await this.secureStorageService.get(`${options.userId}_masterkey_biometric`, options); + } + + async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise { + options = this.reconcileOptions(this.reconcileOptions(options, { keySuffix: 'biometric' }), await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return false; + } + return await this.secureStorageService.has(`${options.userId}_masterkey_biometric`, options); + } + + async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions(this.reconcileOptions(options, { keySuffix: 'biometric' }), await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey_biometric`, value, options); + } + + async getDecodedToken(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.tokens?.decodedToken; + } + + async setDecodedToken(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.tokens.decodedToken = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCiphers(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.ciphers?.decrypted; + } + + async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.ciphers.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCollections(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.collections?.decrypted; + } + + async setDecryptedCollections(value: CollectionView[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.collections.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.cryptoSymmetricKey?.decrypted; + } + + async setDecryptedCryptoSymmetricKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.keys.cryptoSymmetricKey.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedFolders(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.folders?.decrypted; + } + + async setDecryptedFolders(value: FolderView[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.folders.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedOrganizationKeys(options?: StorageOptions): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.organizationKeys?.decrypted; + } + + async setDecryptedOrganizationKeys(value: Map, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.keys.organizationKeys.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPasswordGenerationHistory(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.passwordGenerationHistory?.decrypted; + } + + async setDecryptedPasswordGenerationHistory(value: GeneratedPasswordHistory[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.passwordGenerationHistory.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPinProtected(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.settings?.pinProtected?.decrypted; + } + + async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.settings.pinProtected.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPolicies(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.policies?.decrypted; + } + + async setDecryptedPolicies(value: Policy[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.policies.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPrivateKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.privateKey?.decrypted; + } + + async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.keys.privateKey.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedProviderKeys(options?: StorageOptions): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.providerKeys?.decrypted; + } + + async setDecryptedProviderKeys(value: Map, options?: StorageOptions): Promise { + const account = (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))); + account.keys.providerKeys.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedSends(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.sends?.decrypted; + } + + async setDecryptedSends(value: SendView[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.sends.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDefaultUriMatch(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.defaultUriMatch; + } + + async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.defaultUriMatch = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableAddLoginNotification(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableAddLoginNotification ?? false; + } + + async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableAddLoginNotification = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableAutoBiometricsPrompt(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableAutoBiometricsPrompt ?? false; + } + + async setDisableAutoBiometricsPrompt(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableAutoBiometricsPrompt = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableAutoTotpCopy(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableAutoTotpCopy ?? false; + } + + async setDisableAutoTotpCopy(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableAutoTotpCopy = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableBadgeCounter(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableBadgeCounter ?? false; + } + + async setDisableBadgeCounter(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableBadgeCounter = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableChangedPasswordNotification(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableChangedPasswordNotification ?? false; + } + + async setDisableChangedPasswordNotification(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableChangedPasswordNotification = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableContextMenuItem(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableContextMenuItem ?? false; + } + + async setDisableContextMenuItem(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableContextMenuItem = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDisableFavicon(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.disableFavicon ?? false; + } + + async setDisableFavicon(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + globals.disableFavicon = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getDisableGa(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.disableGa ?? false; + } + + async setDisableGa(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.disableGa = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDontShowCardsCurrentTab(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.dontShowCardsCurrentTab ?? false; + } + + async setDontShowCardsCurrentTab(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.dontShowCardsCurrentTab = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getDontShowIdentitiesCurrentTab(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.dontShowIdentitiesCurrentTab ?? false; + } + + async setDontShowIdentitiesCurrentTab(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.dontShowIdentitiesCurrentTab = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEmail(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.email; + } + + async setEmail(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.email = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEmailVerified(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile.emailVerified ?? false; + } + + async setEmailVerified(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.emailVerified = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableAlwaysOnTop(options?: StorageOptions): Promise { + const accountPreference = (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableAlwaysOnTop; + const globalPreference = (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.enableAlwaysOnTop; + return accountPreference ?? globalPreference ?? false; + } + + async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableAlwaysOnTop = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.enableAlwaysOnTop = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableAutoFillOnPageLoad(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableAutoFillOnPageLoad ?? false; + } + + async setEnableAutoFillOnPageLoad(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableAutoFillOnPageLoad = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableBiometric(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.enableBiometrics ?? false; + } + + async setEnableBiometric(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.enableBiometrics = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableBrowserIntegration(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableBrowserIntegration ?? false; + } + + async setEnableBrowserIntegration(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableBrowserIntegration = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableBrowserIntegrationFingerprint(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableBrowserIntegrationFingerprint ?? false; + } + + async setEnableBrowserIntegrationFingerprint(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableBrowserIntegrationFingerprint = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableCloseToTray(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableCloseToTray ?? false; + } + + async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableCloseToTray = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableFullWidth(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableFullWidth ?? false; + } + + async setEnableFullWidth(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.settings.enableFullWidth = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getEnableGravitars(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableGravitars ?? false; + } + + async setEnableGravitars(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.settings.enableGravitars = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getEnableMinimizeToTray(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableMinimizeToTray ?? false; + } + + async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableMinimizeToTray = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableStartToTray(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings.enableStartToTray ?? false; + } + + async setEnableStartToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableStartToTray = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEnableTray(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.enableTray ?? false; + } + + async setEnableTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.enableTray = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.data?.ciphers?.encrypted; + } + + async setEncryptedCiphers(value: { [id: string]: CipherData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.data.ciphers.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getEncryptedCollections(options?: StorageOptions): Promise<{ [id: string]: CollectionData; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.data?.collections?.encrypted; + } + + async setEncryptedCollections(value: { [id: string]: CollectionData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.data.collections.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys.cryptoSymmetricKey.encrypted; + } + + async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.cryptoSymmetricKey.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedFolders(options?: StorageOptions): Promise<{ [id: string]: FolderData; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.data?.folders?.encrypted; + } + + async setEncryptedFolders(value: { [id: string]: FolderData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.data.folders.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getEncryptedOrganizationKeys(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.organizationKeys.encrypted; + } + + async setEncryptedOrganizationKeys(value: Map, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.organizationKeys.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedPasswordGenerationHistory(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.passwordGenerationHistory?.encrypted; + } + + async setEncryptedPasswordGenerationHistory(value: GeneratedPasswordHistory[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.data.passwordGenerationHistory.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedPinProtected(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.pinProtected?.encrypted; + } + + async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.pinProtected.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedPolicies(options?: StorageOptions): Promise<{ [id: string]: PolicyData; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.policies?.encrypted; + } + + async setEncryptedPolicies(value: { [id: string]: PolicyData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.data.policies.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedPrivateKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.privateKey?.encrypted; + } + + async setEncryptedPrivateKey(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.privateKey.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedProviderKeys(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.providerKeys?.encrypted; + } + + async setEncryptedProviderKeys(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.providerKeys.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.data?.sends.encrypted; + } + + async setEncryptedSends(value: { [id: string]: SendData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.data.sends.encrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getEntityId(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.entityId; + } + + async setEntityId(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.entityId = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getEntityType(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.entityType; + } + + async setEntityType(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.entityType = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getEnvironmentUrls(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.environmentUrls ?? { + server: 'bitwarden.com', + }; + } + + async setEnvironmentUrls(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.environmentUrls = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEquivalentDomains(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.equivalentDomains; + } + + async setEquivalentDomains(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.equivalentDomains = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEventCollection(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.eventCollection; + } + + async setEventCollection(value: EventData[], options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.data.eventCollection = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getEverBeenUnlocked(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.everBeenUnlocked ?? false; + } + + async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.everBeenUnlocked = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getForcePasswordReset(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.forcePasswordReset ?? false; + } + + async setForcePasswordReset(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.forcePasswordReset = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getInstalledVersion(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.installedVersion; + } + + async setInstalledVersion(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.installedVersion = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getIsAuthenticated(options?: StorageOptions): Promise { + return await this.getAccessToken(options) != null && await this.getUserId(options) != null; + } + + async getKdfIterations(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfIterations; + } + + async setKdfIterations(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.kdfIterations = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getKdfType(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfType; + } + + async setKdfType(value: KdfType, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.kdfType = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getKeyHash(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.keyHash; + } + + async setKeyHash(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.profile.keyHash = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getLastActive(options?: StorageOptions): Promise { + const lastActive = (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.lastActive; + return lastActive ?? (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.lastActive; + } + + async setLastActive(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + if (account != null) { + account.profile.lastActive = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.lastActive = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getLastSync(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.profile?.lastSync; + } + + async setLastSync(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.profile.lastSync = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getLegacyEtmKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.legacyEtmKey; + } + + async setLegacyEtmKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.keys.legacyEtmKey = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getLocalData(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.localData; + } + + async setLocalData(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.localData = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getLocale(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.locale; + } + + async setLocale(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + globals.locale = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getLoginRedirect(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.loginRedirect; + } + + async setLoginRedirect(value: any, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)); + globals.loginRedirect = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getMainWindowSize(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.mainWindowSize; + } + + async setMainWindowSize(value: number, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)); + globals.mainWindowSize = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.minimizeOnCopyToClipboard ?? false; + } + + async setMinimizeOnCopyToClipboard(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.minimizeOnCopyToClipboard = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: any; }> { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.neverDomains; + } + + async setNeverDomains(value: { [id: string]: any; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.neverDomains = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getNoAutoPromptBiometrics(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.noAutoPromptBiometrics ?? false; + } + + async setNoAutoPromptBiometrics(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.noAutoPromptBiometrics = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getNoAutoPromptBiometricsText(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.noAutoPromptBiometricsText; + } + + async setNoAutoPromptBiometricsText(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.noAutoPromptBiometricsText = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getOpenAtLogin(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.openAtLogin ?? false; + } + + async setOpenAtLogin(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.openAtLogin = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getOrganizationInvitation(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.organizationInvitation; + } + + async setOrganizationInvitation(value: any, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions)); + globals.organizationInvitation = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData; }> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.organizations; + } + + async setOrganizations(value: { [id: string]: OrganizationData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.organizations = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getPasswordGenerationOptions(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.passwordGenerationOptions; + } + + async setPasswordGenerationOptions(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.passwordGenerationOptions = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getProtectedPin(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.settings?.protectedPin; + } + + async setProtectedPin(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.settings.protectedPin = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getProviders(options?: StorageOptions): Promise<{ [id: string]: ProviderData; }> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.data?.providers; + } + + async setProviders(value: { [id: string]: ProviderData; }, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.data.providers = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getPublicKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.keys?.publicKey; + } + + async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.keys.publicKey = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getRefreshToken(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.tokens?.refreshToken; + } + + async setRefreshToken(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + account.tokens.refreshToken = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + async getRememberedEmail(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.rememberedEmail; + } + + async setRememberedEmail(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + globals.rememberedEmail = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getSecurityStamp(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.tokens.securityStamp = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getSettings(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())))?.settings?.settings; + } + + async setSettings(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + account.settings.settings = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())); + } + + async getSsoCodeVerifier(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.ssoCodeVerifier; + } + + async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.ssoCodeVerifier = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getSsoOrgIdentifier(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.profile?.ssoOrganizationIdentifier; + } + + async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.profile.ssoOrganizationIdentifier = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getSsoState(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.ssoState; + } + + async setSsoState(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.ssoState = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getTheme(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.theme; + } + + async setTheme(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + globals.theme = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getTwoFactorToken(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.twoFactorToken; + } + + async setTwoFactorToken(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + globals.twoFactorToken = value; + await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getUserId(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.userId; + } + + async getUsesKeyConnector(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile?.usesKeyConnector; + } + + async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); + account.profile.usesKeyConnector = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getVaultTimeout(options?: StorageOptions): Promise { + const accountVaultTimeout = (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.vaultTimeout; + const globalVaultTimeout = (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.vaultTimeout; + return accountVaultTimeout ?? globalVaultTimeout ?? 15; + } + + async setVaultTimeout(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.settings.vaultTimeout = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getVaultTimeoutAction(options?: StorageOptions): Promise { + const accountVaultTimeoutAction = (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.vaultTimeoutAction; + const globalVaultTimeoutAction = (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.vaultTimeoutAction; + return accountVaultTimeoutAction ?? globalVaultTimeoutAction; + } + + async setVaultTimeoutAction(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + account.settings.vaultTimeoutAction = value; + await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())); + } + + async getStateVersion(): Promise { + return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1; + } + + async setStateVersion(value: number): Promise { + const globals = await this.getGlobals(await this.defaultOnDiskOptions()); + globals.stateVersion = value; + await this.saveGlobals(globals, await this.defaultOnDiskOptions()); + } + + async getWindow(): Promise> { + const globals = await this.getGlobals(await this.defaultOnDiskOptions()); + return globals?.window != null && Object.keys(globals.window).length > 0 ? + globals.window : + new Map(); + } + + async setWindow(value: Map, options?: StorageOptions): Promise { + const globals = await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())); + globals.window = value; + return await this.saveGlobals(globals, this.reconcileOptions(options, await this.defaultOnDiskOptions())); + } + + private async getGlobals(options: StorageOptions): Promise { + let globals: GlobalState; + if (this.useMemory(options.storageLocation)) { + globals = this.getGlobalsFromMemory(); + } + + if (this.useDisk && globals == null) { + globals = await this.getGlobalsFromDisk(options); + } + + return globals ?? new GlobalState(); + } + + private async saveGlobals(globals: GlobalState, options: StorageOptions) { + return this.useMemory(options.storageLocation) ? + this.saveGlobalsToMemory(globals) : + await this.saveGlobalsToDisk(globals, options); + } + + private getGlobalsFromMemory(): GlobalState { + return this.state.globals; + } + + private async getGlobalsFromDisk(options: StorageOptions): Promise { + return (await this.storageService.get('state', options))?.globals; + } + + private saveGlobalsToMemory(globals: GlobalState): void { + this.state.globals = globals; + } + + private async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise { + if (options.useSecureStorage) { + const state = await this.secureStorageService.get('state', options) ?? new State(); + state.globals = globals; + await this.secureStorageService.save('state', state, options); + } else { + const state = await this.storageService.get('state', options) ?? new State(); + state.globals = globals; + await this.saveStateToStorage(state, options); + } + } + + private async getAccount(options: StorageOptions): Promise { + try { + let account: Account; + if (this.useMemory(options.storageLocation)) { + account = this.getAccountFromMemory(options); + } + + if (this.useDisk(options.storageLocation) && account == null) { + account = await this.getAccountFromDisk(options); + } + + return account != null ? + new Account(account) : + null; + } + catch (e) { + this.logService.error(e); + } + } + + private getAccountFromMemory(options: StorageOptions): Account { + if (this.state.accounts == null) { + return null; + } + return this.state.accounts[this.getUserIdFromMemory(options)]; + } + + private getUserIdFromMemory(options: StorageOptions): string { + return options?.userId != null ? + this.state.accounts[options.userId]?.profile?.userId : + this.state.activeUserId; + } + + private async getAccountFromDisk(options: StorageOptions): Promise { + if (options?.userId == null && this.state.activeUserId == null) { + return null; + } + + const state = options?.useSecureStorage ? + await this.secureStorageService.get('state', options) ?? + await this.storageService.get('state', this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })) : + await this.storageService.get('state', options); + + return state?.accounts[options?.userId ?? this.state.activeUserId]; + } + + private useMemory(storageLocation: StorageLocation) { + return storageLocation === StorageLocation.Memory || + storageLocation === StorageLocation.Both; + } + + private useDisk(storageLocation: StorageLocation) { + return storageLocation === StorageLocation.Disk || + storageLocation === StorageLocation.Both; + } + + private async saveAccount(account: Account, options: StorageOptions = { + storageLocation: StorageLocation.Both, + useSecureStorage: false, + }) { + return this.useMemory(options.storageLocation) ? + await this.saveAccountToMemory(account) : + await this.saveAccountToDisk(account, options); + } + + private async saveAccountToDisk(account: Account, options: StorageOptions): Promise { + const storageLocation = options.useSecureStorage ? + this.secureStorageService : + this.storageService; + + const state = await storageLocation.get('state', options) ?? new State(); + state.accounts[account.profile.userId] = account; + + await storageLocation.save('state', state, options); + await this.pushAccounts(); + } + + private async saveAccountToMemory(account: Account): Promise { + if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) { + this.state.accounts[account.profile.userId] = account; + } + await this.pushAccounts(); + } + + private async scaffoldNewAccountStorage(account: Account): Promise { + await this.scaffoldNewAccountLocalStorage(account); + await this.scaffoldNewAccountSessionStorage(account); + await this.scaffoldNewAccountMemoryStorage(account); + } + + private async scaffoldNewAccountLocalStorage(account: Account): Promise { + const storedState = await this.storageService.get('state', await this.defaultOnDiskLocalOptions()) ?? new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions()); + } + + private async scaffoldNewAccountMemoryStorage(account: Account): Promise { + const storedState = await this.storageService.get('state', await this.defaultOnDiskMemoryOptions()) ?? new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions()); + } + + private async scaffoldNewAccountSessionStorage(account: Account): Promise { + const storedState = await this.storageService.get('state', await this.defaultOnDiskOptions()) ?? new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); + } + + private async pushAccounts(): Promise { + await this.pruneInMemoryAccounts(); + if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) { + this.accounts.next(null); + return; + } + + this.accounts.next(this.state.accounts); + } + + private reconcileOptions(requestedOptions: StorageOptions, defaultOptions: StorageOptions): StorageOptions { + if (requestedOptions == null) { + return defaultOptions; + } + requestedOptions.userId = requestedOptions?.userId ?? defaultOptions.userId; + requestedOptions.storageLocation = requestedOptions?.storageLocation ?? defaultOptions.storageLocation; + requestedOptions.useSecureStorage = requestedOptions?.useSecureStorage ?? defaultOptions.useSecureStorage; + requestedOptions.htmlStorageLocation = requestedOptions?.htmlStorageLocation ?? defaultOptions.htmlStorageLocation; + requestedOptions.keySuffix = requestedOptions?.keySuffix ?? defaultOptions.keySuffix; + return requestedOptions; + } + + private get defaultInMemoryOptions(): StorageOptions { + return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId }; + } + + private async defaultOnDiskOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Session, + userId: await this.getActiveUserIdFromStorage(), + useSecureStorage: false, + }; + } + + private async defaultOnDiskLocalOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Local, + userId: await this.getActiveUserIdFromStorage(), + useSecureStorage: false, + }; + } + + private async defaultOnDiskMemoryOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Memory, + userId: await this.getUserId(), + useSecureStorage: false, + }; + } + + private async defaultSecureStorageOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + useSecureStorage: true, + userId: await this.getActiveUserIdFromStorage(), + }; + } + + private async getActiveUserIdFromStorage(): Promise { + const state = await this.storageService.get('state'); + return state?.activeUserId; + } + + private async removeAccountFromLocalStorage(userId: string = this.state.activeUserId): Promise { + const state = await this.storageService.get('state', { htmlStorageLocation: HtmlStorageLocation.Local }); + if (state?.accounts[userId] == null) { + return; + } + + state.accounts[userId] = new Account({ + settings: state.accounts[userId].settings, + }); + + await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions()); + } + + private async removeAccountFromSessionStorage(userId: string = this.state.activeUserId): Promise { + const state = await this.storageService.get('state', { htmlStorageLocation: HtmlStorageLocation.Session }); + if (state?.accounts[userId] == null) { + return; + } + + state.accounts[userId] = new Account({ + settings: state.accounts[userId].settings, + }); + + await this.saveStateToStorage(state, await this.defaultOnDiskOptions()); + } + + private async removeAccountFromSecureStorage(userId: string = this.state.activeUserId): Promise { + await this.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.setCryptoMasterKeyBiometric(null, { userId: userId }); + await this.setCryptoMasterKeyB64(null, { userId: userId }); + } + + private removeAccountFromMemory(userId: string = this.state.activeUserId): void { + delete this.state.accounts[userId]; + } + + private async saveStateToStorage(state: State, options: StorageOptions): Promise { + await this.storageService.save('state', state, options); + } + + private async pruneInMemoryAccounts() { + // We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state + for (const userId in this.state.accounts) { + if (!await this.getIsAuthenticated({ userId: userId })) { + delete this.state.accounts[userId]; + } + } } } diff --git a/common/src/services/stateMigration.service.ts b/common/src/services/stateMigration.service.ts new file mode 100644 index 0000000000..4b6ceef3ab --- /dev/null +++ b/common/src/services/stateMigration.service.ts @@ -0,0 +1,335 @@ +import { StorageService } from '../abstractions/storage.service'; + +import { Account } from '../models/domain/account'; +import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { State } from '../models/domain/state'; +import { StorageOptions } from '../models/domain/storageOptions'; + +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { EventData } from '../models/data/eventData'; +import { FolderData } from '../models/data/folderData'; +import { OrganizationData } from '../models/data/organizationData'; +import { PolicyData } from '../models/data/policyData'; +import { ProviderData } from '../models/data/providerData'; +import { SendData } from '../models/data/sendData'; + +import { HtmlStorageLocation } from '../enums/htmlStorageLocation'; +import { KdfType } from '../enums/kdfType'; + +// Originally (before January 2022) storage was handled as a flat key/value pair store. +// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration. +const v1Keys = { + accessToken: 'accessToken', + alwaysShowDock: 'alwaysShowDock', + autoConfirmFingerprints: 'autoConfirmFingerprints', + autoFillOnPageLoadDefault: 'autoFillOnPageLoadDefault', + biometricAwaitingAcceptance: 'biometricAwaitingAcceptance', + biometricFingerprintValidated: 'biometricFingerprintValidated', + biometricText: 'biometricText', + biometricUnlock: 'biometric', + clearClipboard: 'clearClipboardKey', + clientId: 'clientId', + clientSecret: 'clientSecret', + collapsedGroupings: 'collapsedGroupings', + convertAccountToKeyConnector: 'convertAccountToKeyConnector', + defaultUriMatch: 'defaultUriMatch', + disableAddLoginNotification: 'disableAddLoginNotification', + disableAutoBiometricsPrompt: 'noAutoPromptBiometrics', + disableAutoTotpCopy: 'disableAutoTotpCopy', + disableBadgeCounter: 'disableBadgeCounter', + disableChangedPasswordNotification: 'disableChangedPasswordNotification', + disableContextMenuItem: 'disableContextMenuItem', + disableFavicon: 'disableFavicon', + disableGa: 'disableGa', + dontShowCardsCurrentTab: 'dontShowCardsCurrentTab', + dontShowIdentitiesCurrentTab: 'dontShowIdentitiesCurrentTab', + emailVerified: 'emailVerified', + enableAlwaysOnTop: 'enableAlwaysOnTopKey', + enableAutoFillOnPageLoad: 'enableAutoFillOnPageLoad', + enableBiometric: 'enabledBiometric', + enableBrowserIntegration: 'enableBrowserIntegration', + enableBrowserIntegrationFingerprint: 'enableBrowserIntegrationFingerprint', + enableCloseToTray: 'enableCloseToTray', + enableFullWidth: 'enableFullWidth', + enableGravatars: 'enableGravatars', + enableMinimizeToTray: 'enableMinimizeToTray', + enableStartToTray: 'enableStartToTrayKey', + enableTray: 'enableTray', + encKey: 'encKey', // Generated Symmetric Key + encOrgKeys: 'encOrgKeys', + encPrivate: 'encPrivateKey', + encProviderKeys: 'encProviderKeys', + entityId: 'entityId', + entityType: 'entityType', + environmentUrls: 'environmentUrls', + equivalentDomains: 'equivalentDomains', + eventCollection: 'eventCollection', + forcePasswordReset: 'forcePasswordReset', + history: 'generatedPasswordHistory', + installedVersion: 'installedVersion', + kdf: 'kdf', + kdfIterations: 'kdfIterations', + key: 'key', // Master Key + keyHash: 'keyHash', + lastActive: 'lastActive', + localData: 'sitesLocalData', + locale: 'locale', + mainWindowSize: 'mainWindowSize', + minimizeOnCopyToClipboard: 'minimizeOnCopyToClipboardKey', + neverDomains: 'neverDomains', + noAutoPromptBiometricsText: 'noAutoPromptBiometricsText', + openAtLogin: 'openAtLogin', + passwordGenerationOptions: 'passwordGenerationOptions', + pinProtected: 'pinProtectedKey', + protectedPin: 'protectedPin', + refreshToken: 'refreshToken', + ssoCodeVerifier: 'ssoCodeVerifier', + ssoIdentifier: 'ssoOrgIdentifier', + ssoState: 'ssoState', + stamp: 'securityStamp', + theme: 'theme', + userEmail: 'userEmail', + userId: 'userId', + usesConnector: 'usesKeyConnector', + vaultTimeoutAction: 'vaultTimeoutAction', + vaultTimeout: 'lockOption', + rememberedEmail: 'rememberedEmail', +}; + +const v1KeyPrefixes = { + ciphers: 'ciphers_', + collections: 'collections_', + folders: 'folders_', + lastSync: 'lastSync_', + policies: 'policies_', + twoFactorToken: 'twoFactorToken_', + organizations: 'organizations_', + providers: 'providers_', + sends: 'sends_', + settings: 'settings_', +}; + + +export class StateMigrationService { + readonly latestVersion: number = 2; + + constructor( + private storageService: StorageService, + private secureStorageService: StorageService + ) {} + + async needsMigration(): Promise { + const currentStateVersion = (await this.storageService.get('state'))?.globals?.stateVersion; + return currentStateVersion == null || currentStateVersion < this.latestVersion; + } + + async migrate(): Promise { + let currentStateVersion = (await this.storageService.get('state'))?.globals?.stateVersion ?? 1; + while (currentStateVersion < this.latestVersion) { + switch (currentStateVersion) { + case 1: + await this.migrateStateFrom1To2(); + break; + } + + currentStateVersion += 1; + } + } + + private async migrateStateFrom1To2(): Promise { + const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local }; + const userId = await this.storageService.get('userId'); + const initialState: State = userId == null ? + { + globals: { + stateVersion: 2, + }, + accounts: {}, + activeUserId: null, + } : + { + activeUserId: userId, + globals: { + biometricAwaitingAcceptance: await this.storageService.get(v1Keys.biometricAwaitingAcceptance, options), + biometricFingerprintValidated: await this.storageService.get(v1Keys.biometricFingerprintValidated, options), + biometricText: await this.storageService.get(v1Keys.biometricText, options), + disableFavicon: await this.storageService.get(v1Keys.disableFavicon, options), + enableAlwaysOnTop: await this.storageService.get(v1Keys.enableAlwaysOnTop, options), + enableBiometrics: await this.storageService.get(v1Keys.enableBiometric, options), + installedVersion: await this.storageService.get(v1Keys.installedVersion, options), + lastActive: await this.storageService.get(v1Keys.lastActive, options), + locale: await this.storageService.get(v1Keys.locale, options), + loginRedirect: null, + mainWindowSize: null, + noAutoPromptBiometrics: await this.storageService.get(v1Keys.disableAutoBiometricsPrompt, options), + noAutoPromptBiometricsText: await this.storageService.get(v1Keys.noAutoPromptBiometricsText, options), + openAtLogin: await this.storageService.get(v1Keys.openAtLogin, options), + organizationInvitation: await this.storageService.get('', options), + rememberedEmail: await this.storageService.get(v1Keys.rememberedEmail, options), + stateVersion: 2, + theme: await this.storageService.get(v1Keys.theme, options), + twoFactorToken: await this.storageService.get(v1KeyPrefixes.twoFactorToken + userId, options), + vaultTimeout: await this.storageService.get(v1Keys.vaultTimeout, options), + vaultTimeoutAction: await this.storageService.get(v1Keys.vaultTimeoutAction, options), + window: null, + }, + accounts: { + [userId]: new Account({ + data: { + addEditCipherInfo: null, + ciphers: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: CipherData }>(v1KeyPrefixes.ciphers + userId, options), + }, + collapsedGroupings: null, + collections: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: CollectionData }>(v1KeyPrefixes.collections + userId, options), + }, + eventCollection: await this.storageService.get(v1Keys.eventCollection, options), + folders: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: FolderData }>(v1KeyPrefixes.folders + userId, options), + }, + localData: null, + organizations: await this.storageService.get<{ [id: string]: OrganizationData }>(v1KeyPrefixes.organizations + userId), + passwordGenerationHistory: { + decrypted: null, + encrypted: await this.storageService.get('TODO', options), // TODO: Whats up here? + }, + policies: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: PolicyData }>(v1KeyPrefixes.policies + userId, options), + }, + providers: await this.storageService.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId), + sends: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: SendData }>(v1KeyPrefixes.sends, options), + }, + }, + keys: { + apiKeyClientSecret: await this.storageService.get(v1Keys.clientSecret, options), + cryptoMasterKey: null, + cryptoMasterKeyAuto: null, + cryptoMasterKeyB64: null, + cryptoMasterKeyBiometric: null, + cryptoSymmetricKey: { + encrypted: await this.storageService.get(v1Keys.encKey, options), + decrypted: null, + }, + legacyEtmKey: null, + organizationKeys: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.encOrgKeys + userId, options), + }, + privateKey: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.encPrivate, options), + }, + providerKeys: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.encProviderKeys + userId, options), + }, + publicKey: null, + }, + profile: { + apiKeyClientId: await this.storageService.get(v1Keys.clientId, options), + authenticationStatus: null, + convertAccountToKeyConnector: await this.storageService.get(v1Keys.convertAccountToKeyConnector, options), + email: await this.storageService.get(v1Keys.userEmail, options), + emailVerified: await this.storageService.get(v1Keys.emailVerified, options), + entityId: null, + entityType: null, + everBeenUnlocked: null, + forcePasswordReset: null, + hasPremiumPersonally: null, + kdfIterations: await this.storageService.get(v1Keys.kdfIterations, options), + kdfType: await this.storageService.get(v1Keys.kdf, options), + keyHash: await this.storageService.get(v1Keys.keyHash, options), + lastActive: await this.storageService.get(v1Keys.lastActive, options), + lastSync: null, + ssoCodeVerifier: await this.storageService.get(v1Keys.ssoCodeVerifier, options), + ssoOrganizationIdentifier: await this.storageService.get(v1Keys.ssoIdentifier, options), + ssoState: null, + userId: userId, + usesKeyConnector: null, + }, + settings: { + alwaysShowDock: await this.storageService.get(v1Keys.alwaysShowDock, options), + autoConfirmFingerPrints: await this.storageService.get(v1Keys.autoConfirmFingerprints, options), + autoFillOnPageLoadDefault: await this.storageService.get(v1Keys.autoFillOnPageLoadDefault, options), + biometricLocked: null, + biometricUnlock: await this.storageService.get(v1Keys.biometricUnlock, options), + clearClipboard: await this.storageService.get(v1Keys.clearClipboard, options), + defaultUriMatch: await this.storageService.get(v1Keys.defaultUriMatch, options), + disableAddLoginNotification: await this.storageService.get(v1Keys.disableAddLoginNotification, options), + disableAutoBiometricsPrompt: await this.storageService.get(v1Keys.disableAutoBiometricsPrompt, options), + disableAutoTotpCopy: await this.storageService.get(v1Keys.disableAutoTotpCopy, options), + disableBadgeCounter: await this.storageService.get(v1Keys.disableBadgeCounter, options), + disableChangedPasswordNotification: await this.storageService.get(v1Keys.disableChangedPasswordNotification, options), + disableContextMenuItem: await this.storageService.get(v1Keys.disableContextMenuItem, options), + disableGa: await this.storageService.get(v1Keys.disableGa, options), + dontShowCardsCurrentTab: await this.storageService.get(v1Keys.dontShowCardsCurrentTab, options), + dontShowIdentitiesCurrentTab: await this.storageService.get(v1Keys.dontShowIdentitiesCurrentTab, options), + enableAlwaysOnTop: await this.storageService.get(v1Keys.enableAlwaysOnTop, options), + enableAutoFillOnPageLoad: await this.storageService.get(v1Keys.enableAutoFillOnPageLoad, options), + enableBiometric: await this.storageService.get(v1Keys.enableBiometric, options), + enableBrowserIntegration: await this.storageService.get(v1Keys.enableBrowserIntegration, options), + enableBrowserIntegrationFingerprint: await this.storageService.get(v1Keys.enableBrowserIntegrationFingerprint, options), + enableCloseToTray: await this.storageService.get(v1Keys.enableCloseToTray, options), + enableFullWidth: await this.storageService.get(v1Keys.enableFullWidth, options), + enableGravitars: await this.storageService.get(v1Keys.enableGravatars, options), + enableMinimizeToTray: await this.storageService.get(v1Keys.enableMinimizeToTray, options), + enableStartToTray: await this.storageService.get(v1Keys.enableStartToTray, options), + enableTray: await this.storageService.get(v1Keys.enableTray, options), + environmentUrls: await this.storageService.get(v1Keys.environmentUrls, options), + equivalentDomains: await this.storageService.get(v1Keys.equivalentDomains, options), + minimizeOnCopyToClipboard: await this.storageService.get(v1Keys.minimizeOnCopyToClipboard, options), + neverDomains: await this.storageService.get(v1Keys.neverDomains, options), + openAtLogin: await this.storageService.get(v1Keys.openAtLogin, options), + passwordGenerationOptions: await this.storageService.get(v1Keys.passwordGenerationOptions, options), + pinProtected: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.pinProtected, options), + }, + protectedPin: await this.storageService.get(v1Keys.protectedPin, options), + settings: await this.storageService.get(v1KeyPrefixes.settings + userId, options), + vaultTimeout: await this.storageService.get(v1Keys.vaultTimeout, options), + vaultTimeoutAction: await this.storageService.get(v1Keys.vaultTimeoutAction, options), + }, + tokens: { + accessToken: await this.storageService.get(v1Keys.accessToken, options), + decodedToken: null, + refreshToken: await this.storageService.get(v1Keys.refreshToken, options), + securityStamp: null, + }, + }), + }, + }; + + await this.storageService.save('state', initialState, options); + + if (await this.secureStorageService.has(v1Keys.key, { keySuffix: 'biometric' })) { + await this.secureStorageService.save( + `${userId}_masterkey_biometric`, + await this.secureStorageService.get(v1Keys.key, { keySuffix: 'biometric' }), + { keySuffix: 'biometric' }); + await this.secureStorageService.remove(v1Keys.key, { keySuffix: 'biometric' }); + } + + if (await this.secureStorageService.has(v1Keys.key, { keySuffix: 'auto' })) { + await this.secureStorageService.save( + `${userId}_masterkey_auto`, + await this.secureStorageService.get(v1Keys.key, { keySuffix: 'auto' }), + { keySuffix: 'auto' } + ); + await this.secureStorageService.remove(v1Keys.key, { keySuffix: 'auto' }); + } + + if (await this.secureStorageService.has(v1Keys.key)) { + await this.secureStorageService.save(`${userId}_masterkey`, await this.secureStorageService.get(v1Keys.key)); + await this.secureStorageService.remove(v1Keys.key); + } + } +} diff --git a/common/src/services/sync.service.ts b/common/src/services/sync.service.ts index 8395ddcc87..9ecfc74db4 100644 --- a/common/src/services/sync.service.ts +++ b/common/src/services/sync.service.ts @@ -6,13 +6,13 @@ import { FolderService } from '../abstractions/folder.service'; import { KeyConnectorService } from '../abstractions/keyConnector.service'; import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; +import { OrganizationService } from '../abstractions/organization.service'; import { PolicyService } from '../abstractions/policy.service'; +import { ProviderService } from '../abstractions/provider.service'; import { SendService } from '../abstractions/send.service'; import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; @@ -35,30 +35,24 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SendResponse } from '../models/response/sendResponse'; -const Keys = { - lastSyncPrefix: 'lastSync_', -}; - export class SyncService implements SyncServiceAbstraction { syncInProgress: boolean = false; - constructor(private userService: UserService, private apiService: ApiService, - private settingsService: SettingsService, private folderService: FolderService, - private cipherService: CipherService, private cryptoService: CryptoService, - private collectionService: CollectionService, private storageService: StorageService, - private messagingService: MessagingService, private policyService: PolicyService, + constructor(private apiService: ApiService, private settingsService: SettingsService, + private folderService: FolderService, private cipherService: CipherService, + private cryptoService: CryptoService, private collectionService: CollectionService, + private messagingService: MessagingService, private policyService: PolicyService, private sendService: SendService, private logService: LogService, - private tokenService: TokenService, private keyConnectorService: KeyConnectorService, - private logoutCallback: (expired: boolean) => Promise) { - } + private keyConnectorService: KeyConnectorService, private stateService: StateService, + private organizationService: OrganizationService, private providerService: ProviderService, + private logoutCallback: (expired: boolean) => Promise) { } async getLastSync(): Promise { - const userId = await this.userService.getUserId(); - if (userId == null) { + if (await this.stateService.getUserId() == null) { return null; } - const lastSync = await this.storageService.get(Keys.lastSyncPrefix + userId); + const lastSync = await this.stateService.getLastSync(); if (lastSync) { return new Date(lastSync); } @@ -66,18 +60,13 @@ export class SyncService implements SyncServiceAbstraction { return null; } - async setLastSync(date: Date): Promise { - const userId = await this.userService.getUserId(); - if (userId == null) { - return; - } - - await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); + async setLastSync(date: Date, userId?: string): Promise { + await this.stateService.setLastSync(date.toJSON(), { userId: userId }); } async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { this.syncStarted(); - const isAuthenticated = await this.userService.isAuthenticated(); + const isAuthenticated = await this.stateService.getIsAuthenticated(); if (!isAuthenticated) { return this.syncCompleted(false); } @@ -97,7 +86,7 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(false); } - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); try { await this.apiService.refreshIdentityToken(); const response = await this.apiService.getSync(); @@ -107,7 +96,7 @@ export class SyncService implements SyncServiceAbstraction { await this.syncCollections(response.collections); await this.syncCiphers(userId, response.ciphers); await this.syncSends(userId, response.sends); - await this.syncSettings(userId, response.domains); + await this.syncSettings(response.domains); await this.syncPolicies(response.policies); await this.setLastSync(now); @@ -123,14 +112,14 @@ export class SyncService implements SyncServiceAbstraction { async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { try { const localFolder = await this.folderService.get(notification.id); if ((!isEdit && localFolder == null) || (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)) { const remoteFolder = await this.apiService.getFolder(notification.id); if (remoteFolder != null) { - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); await this.folderService.upsert(new FolderData(remoteFolder, userId)); this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id }); return this.syncCompleted(true); @@ -145,7 +134,7 @@ export class SyncService implements SyncServiceAbstraction { async syncDeleteFolder(notification: SyncFolderNotification): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { await this.folderService.delete(notification.id); this.messagingService.send('syncedDeletedFolder', { folderId: notification.id }); this.syncCompleted(true); @@ -156,7 +145,7 @@ export class SyncService implements SyncServiceAbstraction { async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { try { let shouldUpdate = true; const localCipher = await this.cipherService.get(notification.id); @@ -195,7 +184,7 @@ export class SyncService implements SyncServiceAbstraction { if (shouldUpdate) { const remoteCipher = await this.apiService.getCipher(notification.id); if (remoteCipher != null) { - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); await this.cipherService.upsert(new CipherData(remoteCipher, userId)); this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id }); return this.syncCompleted(true); @@ -214,7 +203,7 @@ export class SyncService implements SyncServiceAbstraction { async syncDeleteCipher(notification: SyncCipherNotification): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { await this.cipherService.delete(notification.id); this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id }); return this.syncCompleted(true); @@ -224,14 +213,14 @@ export class SyncService implements SyncServiceAbstraction { async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { try { const localSend = await this.sendService.get(notification.id); if ((!isEdit && localSend == null) || (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)) { const remoteSend = await this.apiService.getSend(notification.id); if (remoteSend != null) { - const userId = await this.userService.getUserId(); + const userId = await this.stateService.getUserId(); await this.sendService.upsert(new SendData(remoteSend, userId)); this.messagingService.send('syncedUpsertedSend', { sendId: notification.id }); return this.syncCompleted(true); @@ -246,7 +235,7 @@ export class SyncService implements SyncServiceAbstraction { async syncDeleteSend(notification: SyncSendNotification): Promise { this.syncStarted(); - if (await this.userService.isAuthenticated()) { + if (await this.stateService.getIsAuthenticated()) { await this.sendService.delete(notification.id); this.messagingService.send('syncedDeletedSend', { sendId: notification.id }); this.syncCompleted(true); @@ -286,7 +275,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.userService.getSecurityStamp(); + const stamp = await this.stateService.getSecurityStamp(); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -299,9 +288,9 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setEncPrivateKey(response.privateKey); await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.userService.setSecurityStamp(response.securityStamp); - await this.userService.setEmailVerified(response.emailVerified); - await this.userService.setForcePasswordReset(response.forcePasswordReset); + await this.stateService.setSecurityStamp(response.securityStamp); + await this.stateService.setEmailVerified(response.emailVerified); + await this.stateService.setForcePasswordReset(response.forcePasswordReset); await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); const organizations: { [id: string]: OrganizationData; } = {}; @@ -322,8 +311,8 @@ export class SyncService implements SyncServiceAbstraction { }); await Promise.all([ - this.userService.replaceOrganizations(organizations), - this.userService.replaceProviders(providers), + this.organizationService.save(organizations), + this.providerService.save(providers), ]); if (await this.keyConnectorService.userNeedsMigration()) { @@ -365,7 +354,7 @@ export class SyncService implements SyncServiceAbstraction { return await this.sendService.replace(sends); } - private async syncSettings(userId: string, response: DomainsResponse) { + private async syncSettings(response: DomainsResponse) { let eqDomains: string[][] = []; if (response != null && response.equivalentDomains != null) { eqDomains = eqDomains.concat(response.equivalentDomains); diff --git a/common/src/services/system.service.ts b/common/src/services/system.service.ts index ada918132a..afcb7f8b4f 100644 --- a/common/src/services/system.service.ts +++ b/common/src/services/system.service.ts @@ -1,10 +1,7 @@ import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { SystemService as SystemServiceAbstraction } from '../abstractions/system.service'; -import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; - -import { ConstantsService } from './constants.service'; import { Utils } from '../misc/utils'; @@ -13,28 +10,27 @@ export class SystemService implements SystemServiceAbstraction { private clearClipboardTimeout: any = null; private clearClipboardTimeoutFunction: () => Promise = null; - constructor(private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, - private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, - private reloadCallback: () => Promise = null) { + constructor(private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, + private reloadCallback: () => Promise = null, private stateService: StateService) { } - startProcessReload(): void { - if (this.vaultTimeoutService.pinProtectedKey != null || - this.vaultTimeoutService.biometricLocked || + async startProcessReload(): Promise { + if (await this.stateService.getDecryptedPinProtected() != null || + await this.stateService.getBiometricLocked() || this.reloadInterval != null) { return; } this.cancelProcessReload(); this.reloadInterval = setInterval(async () => { let doRefresh = false; - const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); + const lastActive = await this.stateService.getLastActive(); if (lastActive != null) { const diffSeconds = (new Date()).getTime() - lastActive; // Don't refresh if they are still active in the window doRefresh = diffSeconds >= 5000; } const biometricLockedFingerprintValidated = - await this.storageService.get(ConstantsService.biometricFingerprintValidated) && this.vaultTimeoutService.biometricLocked; + await this.stateService.getBiometricFingerprintValidated() && await this.stateService.getBiometricLocked(); if (doRefresh && !biometricLockedFingerprintValidated) { clearInterval(this.reloadInterval); this.reloadInterval = null; @@ -53,7 +49,7 @@ export class SystemService implements SystemServiceAbstraction { } } - clearClipboard(clipboardValue: string, timeoutMs: number = null): void { + async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { if (this.clearClipboardTimeout != null) { clearTimeout(this.clearClipboardTimeout); this.clearClipboardTimeout = null; @@ -61,7 +57,7 @@ export class SystemService implements SystemServiceAbstraction { if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - this.storageService.get(ConstantsService.clearClipboardKey).then(clearSeconds => { + await this.stateService.getClearClipboard().then(clearSeconds => { if (clearSeconds == null) { return; } diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts index 58f42e7d4a..c541c030a0 100644 --- a/common/src/services/token.service.ts +++ b/common/src/services/token.service.ts @@ -1,26 +1,10 @@ -import { ConstantsService } from './constants.service'; - -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; import { Utils } from '../misc/utils'; -const Keys = { - accessToken: 'accessToken', - refreshToken: 'refreshToken', - twoFactorTokenPrefix: 'twoFactorToken_', - clientId: 'apikey_clientId', - clientSecret: 'apikey_clientSecret', -}; - export class TokenService implements TokenServiceAbstraction { - token: string; - decodedToken: any; - refreshToken: string; - clientId: string; - clientSecret: string; - - constructor(private storageService: StorageService) { + constructor(private stateService: StateService) { } async setTokens(accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]): Promise { @@ -33,60 +17,44 @@ export class TokenService implements TokenServiceAbstraction { } async setClientId(clientId: string): Promise { - this.clientId = clientId; - return this.storeTokenValue(Keys.clientId, clientId); + if (await this.skipTokenStorage() || clientId == null) { + return; + } + return await this.stateService.setApiKeyClientId(clientId); } async getClientId(): Promise { - if (this.clientId != null) { - return this.clientId; - } - - this.clientId = await this.storageService.get(Keys.clientId); - return this.clientId; + return await this.stateService.getApiKeyClientId(); } async setClientSecret(clientSecret: string): Promise { - this.clientSecret = clientSecret; - return this.storeTokenValue(Keys.clientSecret, clientSecret); + if (await this.skipTokenStorage() || clientSecret == null) { + return; + } + return await this.stateService.setApiKeyClientSecret(clientSecret); } async getClientSecret(): Promise { - if (this.clientSecret != null) { - return this.clientSecret; - } - - this.clientSecret = await this.storageService.get(Keys.clientSecret); - return this.clientSecret; + return await this.stateService.getApiKeyClientSecret(); } - async setToken(token: string): Promise { - this.token = token; - this.decodedToken = null; - return this.storeTokenValue(Keys.accessToken, token); + async setToken(token: string): Promise { + await this.stateService.setAccessToken(token); } async getToken(): Promise { - if (this.token != null) { - return this.token; - } - - this.token = await this.storageService.get(Keys.accessToken); - return this.token; + return await this.stateService.getAccessToken(); } async setRefreshToken(refreshToken: string): Promise { - this.refreshToken = refreshToken; - return this.storeTokenValue(Keys.refreshToken, refreshToken); + if (await this.skipTokenStorage()) { + return; + } + return await this.stateService.setRefreshToken(refreshToken); } async getRefreshToken(): Promise { - if (this.refreshToken != null) { - return this.refreshToken; - } - - this.refreshToken = await this.storageService.get(Keys.refreshToken); - return this.refreshToken; + return await this.stateService.getRefreshToken(); } async toggleTokens(): Promise { @@ -94,16 +62,12 @@ export class TokenService implements TokenServiceAbstraction { const refreshToken = await this.getRefreshToken(); const clientId = await this.getClientId(); const clientSecret = await this.getClientSecret(); - const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + const timeout = await this.stateService.getVaultTimeout(); + const action = await this.stateService.getVaultTimeoutAction(); + if ((timeout != null || timeout === 0) && action === 'logOut') { // if we have a vault timeout and the action is log out, reset tokens await this.clearToken(); - this.token = token; - this.refreshToken = refreshToken; - this.clientId = clientId; - this.clientSecret = clientSecret; - return; } await this.setToken(token); @@ -112,44 +76,41 @@ export class TokenService implements TokenServiceAbstraction { await this.setClientSecret(clientSecret); } - setTwoFactorToken(token: string, email: string): Promise { - return this.storageService.save(Keys.twoFactorTokenPrefix + email, token); + async setTwoFactorToken(token: string): Promise { + return await this.stateService.setTwoFactorToken(token); } - getTwoFactorToken(email: string): Promise { - return this.storageService.get(Keys.twoFactorTokenPrefix + email); + async getTwoFactorToken(): Promise { + return await this.stateService.getTwoFactorToken(); } - clearTwoFactorToken(email: string): Promise { - return this.storageService.remove(Keys.twoFactorTokenPrefix + email); + async clearTwoFactorToken(): Promise { + return await this.stateService.setTwoFactorToken(null); } - async clearToken(): Promise { - this.token = null; - this.decodedToken = null; - this.refreshToken = null; - this.clientId = null; - this.clientSecret = null; - - await this.storageService.remove(Keys.accessToken); - await this.storageService.remove(Keys.refreshToken); - await this.storageService.remove(Keys.clientId); - await this.storageService.remove(Keys.clientSecret); + async clearToken(userId?: string): Promise { + await this.stateService.setAccessToken(null, { userId: userId }); + await this.stateService.setRefreshToken(null, { userId: userId }); + await this.stateService.setApiKeyClientId(null, { userId: userId }); + await this.stateService.setApiKeyClientSecret(null, { userId: userId }); } // jwthelper methods // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - decodeToken(): any { - if (this.decodedToken) { - return this.decodedToken; + async decodeToken(token?: string): Promise { + const storedToken = await this.stateService.getDecodedToken(); + if (token === null && storedToken != null) { + return storedToken; } - if (this.token == null) { + token = token ?? await this.stateService.getAccessToken(); + + if (token == null) { throw new Error('Token not found.'); } - const parts = this.token.split('.'); + const parts = token.split('.'); if (parts.length !== 3) { throw new Error('JWT must have 3 parts'); } @@ -159,12 +120,12 @@ export class TokenService implements TokenServiceAbstraction { throw new Error('Cannot decode the token'); } - this.decodedToken = JSON.parse(decoded); - return this.decodedToken; + const decodedToken = JSON.parse(decoded); + return decodedToken; } - getTokenExpirationDate(): Date { - const decoded = this.decodeToken(); + async getTokenExpirationDate(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.exp === 'undefined') { return null; } @@ -174,8 +135,8 @@ export class TokenService implements TokenServiceAbstraction { return d; } - tokenSecondsRemaining(offsetSeconds: number = 0): number { - const d = this.getTokenExpirationDate(); + async tokenSecondsRemaining(offsetSeconds: number = 0): Promise { + const d = await this.getTokenExpirationDate(); if (d == null) { return 0; } @@ -184,13 +145,13 @@ export class TokenService implements TokenServiceAbstraction { return Math.round(msRemaining / 1000); } - tokenNeedsRefresh(minutes: number = 5): boolean { - const sRemaining = this.tokenSecondsRemaining(); + async tokenNeedsRefresh(minutes: number = 5): Promise { + const sRemaining = await this.tokenSecondsRemaining(); return sRemaining < (60 * minutes); } - getUserId(): string { - const decoded = this.decodeToken(); + async getUserId(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.sub === 'undefined') { throw new Error('No user id found'); } @@ -198,8 +159,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.sub as string; } - getEmail(): string { - const decoded = this.decodeToken(); + async getEmail(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.email === 'undefined') { throw new Error('No email found'); } @@ -207,8 +168,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.email as string; } - getEmailVerified(): boolean { - const decoded = this.decodeToken(); + async getEmailVerified(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.email_verified === 'undefined') { throw new Error('No email verification found'); } @@ -216,8 +177,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.email_verified as boolean; } - getName(): string { - const decoded = this.decodeToken(); + async getName(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.name === 'undefined') { return null; } @@ -225,8 +186,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.name as string; } - getPremium(): boolean { - const decoded = this.decodeToken(); + async getPremium(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.premium === 'undefined') { return false; } @@ -234,8 +195,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.premium as boolean; } - getIssuer(): string { - const decoded = this.decodeToken(); + async getIssuer(): Promise { + const decoded = await this.decodeToken(); if (typeof decoded.iss === 'undefined') { throw new Error('No issuer found'); } @@ -243,8 +204,8 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss as string; } - getIsExternal(): boolean { - const decoded = this.decodeToken(); + async getIsExternal(): Promise { + const decoded = await this.decodeToken(); if (!Array.isArray(decoded.amr)) { throw new Error('No amr found'); } @@ -252,18 +213,9 @@ export class TokenService implements TokenServiceAbstraction { return decoded.amr.includes('external'); } - private async storeTokenValue(key: string, value: string) { - if (await this.skipTokenStorage()) { - // if we have a vault timeout and the action is log out, don't store token - return; - } - - return this.storageService.save(key, value); - } - private async skipTokenStorage(): Promise { - const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + const timeout = await this.stateService.getVaultTimeout(); + const action = await this.stateService.getVaultTimeoutAction(); return timeout != null && action === 'logOut'; } } diff --git a/common/src/services/totp.service.ts b/common/src/services/totp.service.ts index 50c8c4eb51..a16c49f614 100644 --- a/common/src/services/totp.service.ts +++ b/common/src/services/totp.service.ts @@ -1,8 +1,6 @@ -import { ConstantsService } from './constants.service'; - import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { LogService } from '../abstractions/log.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; import { Utils } from '../misc/utils'; @@ -11,8 +9,8 @@ const B32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const SteamChars = '23456789BCDFGHJKMNPQRTVWXY'; export class TotpService implements TotpServiceAbstraction { - constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService, - private logService: LogService) { } + constructor(private cryptoFunctionService: CryptoFunctionService, private logService: LogService, + private stateService: StateService) { } async getCode(key: string): Promise { if (key == null) { @@ -114,7 +112,7 @@ export class TotpService implements TotpServiceAbstraction { } async isAutoCopyEnabled(): Promise { - return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); + return !(await this.stateService.getDisableAutoTotpCopy()); } // Helpers diff --git a/common/src/services/user.service.ts b/common/src/services/user.service.ts deleted file mode 100644 index a572691afd..0000000000 --- a/common/src/services/user.service.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService as UserServiceAbstraction } from '../abstractions/user.service'; - -import { OrganizationData } from '../models/data/organizationData'; -import { Organization } from '../models/domain/organization'; - -import { KdfType } from '../enums/kdfType'; - -import { ProviderData } from '../models/data/providerData'; -import { Provider } from '../models/domain/provider'; - -const Keys = { - userId: 'userId', - userEmail: 'userEmail', - stamp: 'securityStamp', - kdf: 'kdf', - kdfIterations: 'kdfIterations', - organizationsPrefix: 'organizations_', - providersPrefix: 'providers_', - emailVerified: 'emailVerified', - forcePasswordReset: 'forcePasswordReset', -}; - -export class UserService implements UserServiceAbstraction { - private userId: string; - private email: string; - private stamp: string; - private kdf: KdfType; - private kdfIterations: number; - private emailVerified: boolean; - private forcePasswordReset: boolean; - - constructor(private tokenService: TokenService, private storageService: StorageService) { } - - async setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise { - this.email = email; - this.userId = userId; - this.kdf = kdf; - this.kdfIterations = kdfIterations; - - await this.storageService.save(Keys.userEmail, email); - await this.storageService.save(Keys.userId, userId); - await this.storageService.save(Keys.kdf, kdf); - await this.storageService.save(Keys.kdfIterations, kdfIterations); - } - - setSecurityStamp(stamp: string): Promise { - this.stamp = stamp; - return this.storageService.save(Keys.stamp, stamp); - } - - setEmailVerified(emailVerified: boolean) { - this.emailVerified = emailVerified; - return this.storageService.save(Keys.emailVerified, emailVerified); - } - - setForcePasswordReset(forcePasswordReset: boolean) { - this.forcePasswordReset = forcePasswordReset; - return this.storageService.save(Keys.forcePasswordReset, forcePasswordReset); - } - - async getUserId(): Promise { - if (this.userId == null) { - this.userId = await this.storageService.get(Keys.userId); - } - return this.userId; - } - - async getEmail(): Promise { - if (this.email == null) { - this.email = await this.storageService.get(Keys.userEmail); - } - return this.email; - } - - async getSecurityStamp(): Promise { - if (this.stamp == null) { - this.stamp = await this.storageService.get(Keys.stamp); - } - return this.stamp; - } - - async getKdf(): Promise { - if (this.kdf == null) { - this.kdf = await this.storageService.get(Keys.kdf); - } - return this.kdf; - } - - async getKdfIterations(): Promise { - if (this.kdfIterations == null) { - this.kdfIterations = await this.storageService.get(Keys.kdfIterations); - } - return this.kdfIterations; - } - - async getEmailVerified(): Promise { - if (this.emailVerified == null) { - this.emailVerified = await this.storageService.get(Keys.emailVerified); - } - return this.emailVerified; - } - - async getForcePasswordReset(): Promise { - if (this.forcePasswordReset == null) { - this.forcePasswordReset = await this.storageService.get(Keys.forcePasswordReset); - } - return this.forcePasswordReset; - } - - async clear(): Promise { - const userId = await this.getUserId(); - - await this.storageService.remove(Keys.userId); - await this.storageService.remove(Keys.userEmail); - await this.storageService.remove(Keys.stamp); - await this.storageService.remove(Keys.kdf); - await this.storageService.remove(Keys.kdfIterations); - await this.storageService.remove(Keys.forcePasswordReset); - await this.clearOrganizations(userId); - await this.clearProviders(userId); - - this.userId = this.email = this.stamp = null; - this.kdf = null; - this.kdfIterations = null; - } - - async isAuthenticated(): Promise { - const token = await this.tokenService.getToken(); - if (token == null) { - return false; - } - - const userId = await this.getUserId(); - return userId != null; - } - - async canAccessPremium(): Promise { - const authed = await this.isAuthenticated(); - if (!authed) { - return false; - } - - const tokenPremium = this.tokenService.getPremium(); - if (tokenPremium) { - return true; - } - - const orgs = await this.getAllOrganizations(); - for (let i = 0; i < orgs.length; i++) { - if (orgs[i].usersGetPremium && orgs[i].enabled) { - return true; - } - } - return false; - } - - async canManageSponsorships(): Promise { - const orgs = await this.getAllOrganizations(); - return orgs.some(o => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null); - } - - async getOrganization(id: string): Promise { - const userId = await this.getUserId(); - const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( - Keys.organizationsPrefix + userId); - if (organizations == null || !organizations.hasOwnProperty(id)) { - return null; - } - - return new Organization(organizations[id]); - } - - async getOrganizationByIdentifier(identifier: string): Promise { - const organizations = await this.getAllOrganizations(); - if (organizations == null || organizations.length === 0) { - return null; - } - - return organizations.find(o => o.identifier === identifier); - } - - async getAllOrganizations(): Promise { - const userId = await this.getUserId(); - const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( - Keys.organizationsPrefix + userId); - const response: Organization[] = []; - for (const id in organizations) { - if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { - response.push(new Organization(organizations[id])); - } - } - return response; - } - - async replaceOrganizations(organizations: { [id: string]: OrganizationData; }): Promise { - const userId = await this.getUserId(); - await this.storageService.save(Keys.organizationsPrefix + userId, organizations); - } - - async clearOrganizations(userId: string): Promise { - await this.storageService.remove(Keys.organizationsPrefix + userId); - } - - async getProvider(id: string): Promise { - const userId = await this.getUserId(); - const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( - Keys.providersPrefix + userId); - if (providers == null || !providers.hasOwnProperty(id)) { - return null; - } - - return new Provider(providers[id]); - } - - async getAllProviders(): Promise { - const userId = await this.getUserId(); - const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( - Keys.providersPrefix + userId); - const response: Provider[] = []; - for (const id in providers) { - if (providers.hasOwnProperty(id)) { - response.push(new Provider(providers[id])); - } - } - return response; - } - - async replaceProviders(providers: { [id: string]: ProviderData; }): Promise { - const userId = await this.getUserId(); - await this.storageService.save(Keys.providersPrefix + userId, providers); - } - - async clearProviders(userId: string): Promise { - await this.storageService.remove(Keys.providersPrefix + userId); - } -} diff --git a/common/src/services/vaultTimeout.service.ts b/common/src/services/vaultTimeout.service.ts index 54d288b155..e88e8293e3 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -1,5 +1,3 @@ -import { ConstantsService } from './constants.service'; - import { CipherService } from '../abstractions/cipher.service'; import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; @@ -9,29 +7,31 @@ import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { PolicyService } from '../abstractions/policy.service'; import { SearchService } from '../abstractions/search.service'; -import { StorageService } from '../abstractions/storage.service'; +import { StateService } from '../abstractions/state.service'; import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service'; +import { KeySuffixOptions } from '../enums/keySuffixOptions'; import { PolicyType } from '../enums/policyType'; -import { EncString } from '../models/domain/encString'; export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { - pinProtectedKey: EncString = null; - biometricLocked: boolean = true; - everBeenUnlocked: boolean = false; - private inited = false; - constructor(private cipherService: CipherService, private folderService: FolderService, - private collectionService: CollectionService, private cryptoService: CryptoService, - protected platformUtilsService: PlatformUtilsService, private storageService: StorageService, - private messagingService: MessagingService, private searchService: SearchService, - private userService: UserService, private tokenService: TokenService, private policyService: PolicyService, + constructor( + private cipherService: CipherService, + private folderService: FolderService, + private collectionService: CollectionService, + private cryptoService: CryptoService, + protected platformUtilsService: PlatformUtilsService, + private messagingService: MessagingService, + private searchService: SearchService, + private tokenService: TokenService, + private policyService: PolicyService, private keyConnectorService: KeyConnectorService, - private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { - } + private stateService: StateService, + private lockedCallback: () => Promise = null, + private loggedOutCallback: (userId?: string) => Promise = null + ) {} init(checkOnInterval: boolean) { if (this.inited) { @@ -50,110 +50,96 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } // Keys aren't stored for a device that is locked or logged out. - async isLocked(): Promise { - // Handle never lock startup situation - if (await this.cryptoService.hasKeyStored('auto') && !this.everBeenUnlocked) { - await this.cryptoService.getKey('auto'); + async isLocked(userId?: string): Promise { + const neverLock = await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId) && + !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + if (neverLock) { + // TODO: This also _sets_ the key so when we check memory in the next line it finds a key. + // We should refactor here. + await this.cryptoService.getKey(KeySuffixOptions.Auto, userId); } - return !this.cryptoService.hasKeyInMemory(); + return !(await this.cryptoService.hasKeyInMemory(userId)); } async checkVaultTimeout(): Promise { if (await this.platformUtilsService.isViewOpen()) { - // Do not lock return; } - // "is logged out check" - similar to isLocked, below - const authed = await this.userService.isAuthenticated(); - if (!authed) { - return; - } - - if (await this.isLocked()) { - return; - } - - const vaultTimeout = await this.getVaultTimeout(); - if (vaultTimeout == null || vaultTimeout < 0) { - return; - } - - const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); - if (lastActive == null) { - return; - } - - const vaultTimeoutSeconds = vaultTimeout * 60; - const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; - if (diffSeconds >= vaultTimeoutSeconds) { - // Pivot based on the saved vault timeout action - const timeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true); + for (const userId in this.stateService.accounts.getValue()) { + if (userId != null && await this.shouldLock(userId)) { + await this.executeTimeoutAction(userId); + } } } - async lock(allowSoftLock = false): Promise { - const authed = await this.userService.isAuthenticated(); + async lock(allowSoftLock = false, userId?: string): Promise { + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; } if (await this.keyConnectorService.getUsesKeyConnector()) { const pinSet = await this.isPinLockSet(); - const pinLock = (pinSet[0] && this.pinProtectedKey != null) || pinSet[1]; + const pinLock = (pinSet[0] && await this.stateService.getDecryptedPinProtected() != null) || pinSet[1]; if (!pinLock && !await this.isBiometricLockSet()) { await this.logOut(); } } - this.biometricLocked = true; - this.everBeenUnlocked = true; - await this.cryptoService.clearKey(false); - await this.cryptoService.clearOrgKeys(true); - await this.cryptoService.clearKeyPair(true); - await this.cryptoService.clearEncKey(true); + if (userId == null || userId === await this.stateService.getUserId()) { + this.searchService.clearIndex(); + } + + await this.stateService.setEverBeenUnlocked(true, { userId: userId }); + await this.stateService.setBiometricLocked(true, { userId: userId }); + + await this.cryptoService.clearKey(false, userId); + await this.cryptoService.clearOrgKeys(true, userId); + await this.cryptoService.clearKeyPair(true, userId); + await this.cryptoService.clearEncKey(true, userId); + + await this.folderService.clearCache(userId); + await this.cipherService.clearCache(userId); + await this.collectionService.clearCache(userId); + + this.messagingService.send('locked', { userId: userId }); - this.folderService.clearCache(); - this.cipherService.clearCache(); - this.collectionService.clearCache(); - this.searchService.clearIndex(); - this.messagingService.send('locked'); if (this.lockedCallback != null) { await this.lockedCallback(); } } - async logOut(): Promise { + async logOut(userId?: string): Promise { if (this.loggedOutCallback != null) { - await this.loggedOutCallback(); + await this.loggedOutCallback(userId); } } async setVaultTimeoutOptions(timeout: number, action: string): Promise { - await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); - await this.storageService.save(ConstantsService.vaultTimeoutActionKey, action); + await this.stateService.setVaultTimeout(timeout); + await this.stateService.setVaultTimeoutAction(action); await this.cryptoService.toggleKey(); await this.tokenService.toggleTokens(); } async isPinLockSet(): Promise<[boolean, boolean]> { - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + const protectedPin = await this.stateService.getProtectedPin(); + const pinProtectedKey = await this.stateService.getEncryptedPinProtected(); return [protectedPin != null, pinProtectedKey != null]; } async isBiometricLockSet(): Promise { - return await this.storageService.get(ConstantsService.biometricUnlockKey); + return await this.stateService.getBiometricUnlock(); } - async getVaultTimeout(): Promise { - const vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + async getVaultTimeout(userId?: string): Promise { + const vaultTimeout = await this.stateService.getVaultTimeout( { userId: userId } ); - if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout)) { - const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout); + if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)) { + const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId); // Remove negative values, and ensure it's smaller than maximum allowed value according to policy let timeout = Math.min(vaultTimeout, policy[0].data.minutes); @@ -163,7 +149,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { // We really shouldn't need to set the value here, but multiple services relies on this value being correct. if (vaultTimeout !== timeout) { - await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); + await this.stateService.setVaultTimeout(timeout, { userId: userId }); } return timeout; @@ -172,9 +158,42 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return vaultTimeout; } - clear(): Promise { - this.everBeenUnlocked = false; - this.pinProtectedKey = null; - return this.storageService.remove(ConstantsService.protectedPin); + async clear(userId?: string): Promise { + await this.stateService.setEverBeenUnlocked(false, { userId: userId }); + await this.stateService.setDecryptedPinProtected(null, { userId: userId }); + await this.stateService.setProtectedPin(null, { userId: userId }); + } + + private async isLoggedOut(userId?: string): Promise { + return !(await this.stateService.getIsAuthenticated({ userId: userId })); + } + + private async shouldLock(userId: string): Promise { + if (await this.isLoggedOut(userId)) { + return false; + } + + if (await this.isLocked(userId)) { + return false; + } + + const vaultTimeout = await this.getVaultTimeout(userId); + if (vaultTimeout == null || vaultTimeout < 0) { + return false; + } + + const lastActive = await this.stateService.getLastActive({ userId: userId }); + if (lastActive == null) { + return false; + } + + const vaultTimeoutSeconds = vaultTimeout * 60; + const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; + return diffSeconds >= vaultTimeoutSeconds; + } + + private async executeTimeoutAction(userId: string): Promise { + const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); + timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true, userId); } } diff --git a/electron/src/biometric.darwin.main.ts b/electron/src/biometric.darwin.main.ts index 26d3bd74ac..162c772230 100644 --- a/electron/src/biometric.darwin.main.ts +++ b/electron/src/biometric.darwin.main.ts @@ -1,21 +1,18 @@ import { ipcMain, systemPreferences } from 'electron'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { BiometricMain } from 'jslib-common/abstractions/biometric.main'; -import { ElectronConstants } from './electronConstants'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; export default class BiometricDarwinMain implements BiometricMain { isError: boolean = false; - constructor(private storageService: StorageService, private i18nservice: I18nService) {} + constructor(private i18nservice: I18nService, private stateService: StateService) {} async init() { - this.storageService.save(ElectronConstants.enableBiometric, await this.supportsBiometric()); - this.storageService.save(ConstantsService.biometricText, 'unlockWithTouchId'); - this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptTouchId'); + await this.stateService.setEnableBiometric(await this.supportsBiometric()); + await this.stateService.setBiometricText('unlockWithTouchId'); + await this.stateService.setNoAutoPromptBiometricsText('noAutoPromptTouchId'); ipcMain.on('biometric', async (event: any, message: any) => { event.returnValue = await this.authenticateBiometric(); diff --git a/electron/src/biometric.windows.main.ts b/electron/src/biometric.windows.main.ts index d33875b757..2bf323883f 100644 --- a/electron/src/biometric.windows.main.ts +++ b/electron/src/biometric.windows.main.ts @@ -1,22 +1,20 @@ import { ipcMain } from 'electron'; import forceFocus from 'forcefocus'; -import { ElectronConstants } from './electronConstants'; import { WindowMain } from './window.main'; import { BiometricMain } from 'jslib-common/abstractions/biometric.main'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; export default class BiometricWindowsMain implements BiometricMain { isError: boolean = false; private windowsSecurityCredentialsUiModule: any; - constructor(private storageService: StorageService, private i18nservice: I18nService, private windowMain: WindowMain, - private logService: LogService) { } + constructor(private i18nservice: I18nService, private windowMain: WindowMain, + private stateService: StateService, private logService: LogService) { } async init() { this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); @@ -27,9 +25,9 @@ export default class BiometricWindowsMain implements BiometricMain { // store error state so we can let the user know on the settings page this.isError = true; } - this.storageService.save(ElectronConstants.enableBiometric, supportsBiometric); - this.storageService.save(ConstantsService.biometricText, 'unlockWithWindowsHello'); - this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptWindowsHello'); + await this.stateService.setEnableBiometric(supportsBiometric); + await this.stateService.setBiometricText('unlockWithWindowsHello'); + await this.stateService.setNoAutoPromptBiometricsText('noAutoPromptWindowsHello'); ipcMain.on('biometric', async (event: any, message: any) => { event.returnValue = await this.authenticateBiometric(); diff --git a/electron/src/electronConstants.ts b/electron/src/electronConstants.ts deleted file mode 100644 index 68be19562a..0000000000 --- a/electron/src/electronConstants.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class ElectronConstants { - static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; - static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; - static readonly enableTrayKey: string = 'enableTray'; - static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; - static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; - static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; - static readonly enableBiometric: string = 'enabledBiometric'; - static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; - static readonly enableBrowserIntegrationFingerprint: string = 'enableBrowserIntegrationFingerprint'; - static readonly alwaysShowDock: string = 'alwaysShowDock'; - static readonly openAtLogin: string = 'openAtLogin'; - static readonly noAutoPromptBiometricsText: string = 'noAutoPromptBiometricsText'; -} diff --git a/electron/src/services/electronCrypto.service.ts b/electron/src/services/electronCrypto.service.ts index dc602e0fbc..36834549ad 100644 --- a/electron/src/services/electronCrypto.service.ts +++ b/electron/src/services/electronCrypto.service.ts @@ -1,16 +1,19 @@ import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { KeySuffixOptions, StorageService } from 'jslib-common/abstractions/storage.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; + +import { CryptoService } from 'jslib-common/services/crypto.service'; + +import { KeySuffixOptions } from 'jslib-common/enums/keySuffixOptions'; +import { StorageLocation } from 'jslib-common/enums/storageLocation'; import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; -import { CryptoService, Keys } from 'jslib-common/services/crypto.service'; export class ElectronCryptoService extends CryptoService { - constructor(storageService: StorageService, secureStorageService: StorageService, - cryptoFunctionService: CryptoFunctionService, platformUtilService: PlatformUtilsService, - logService: LogService) { - super(storageService, secureStorageService, cryptoFunctionService, platformUtilService, logService); + constructor(cryptoFunctionService: CryptoFunctionService, platformUtilService: PlatformUtilsService, + logService: LogService, stateService: StateService) { + super(cryptoFunctionService, platformUtilService, logService, stateService); } async hasKeyStored(keySuffix: KeySuffixOptions): Promise { @@ -18,17 +21,17 @@ export class ElectronCryptoService extends CryptoService { return super.hasKeyStored(keySuffix); } - protected async storeKey(key: SymmetricCryptoKey) { - if (await this.shouldStoreKey('auto')) { - await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'auto' }); + protected async storeKey(key: SymmetricCryptoKey, userId?: string) { + if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) { + await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId }); } else { - this.clearStoredKey('auto'); + this.clearStoredKey(KeySuffixOptions.Auto); } - if (await this.shouldStoreKey('biometric')) { - await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'biometric' }); + if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) { + await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId }); } else { - this.clearStoredKey('biometric'); + this.clearStoredKey(KeySuffixOptions.Biometric); } } @@ -43,24 +46,24 @@ export class ElectronCryptoService extends CryptoService { */ private async upgradeSecurelyStoredKey() { // attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway. - const key = await this.secureStorageService.get(Keys.key); + const key = await this.stateService.getCryptoMasterKeyB64(); if (key == null) { return; } try { - if (await this.shouldStoreKey('auto')) { - await this.secureStorageService.save(Keys.key, key, { keySuffix: 'auto' }); + if (await this.shouldStoreKey(KeySuffixOptions.Auto)) { + await this.stateService.setCryptoMasterKeyAuto(key); } - if (await this.shouldStoreKey('biometric')) { - await this.secureStorageService.save(Keys.key, key, { keySuffix: 'biometric' }); + if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) { + await this.stateService.setCryptoMasterKeyBiometric(key); } } catch (e) { this.logService.error(`Encountered error while upgrading obsolete Bitwarden secure storage item:`); this.logService.error(e); } - await this.secureStorageService.remove(Keys.key); + await this.stateService.setCryptoMasterKeyB64(null); } } diff --git a/electron/src/services/electronPlatformUtils.service.ts b/electron/src/services/electronPlatformUtils.service.ts index 7ffe3e3f94..acd910c2f7 100644 --- a/electron/src/services/electronPlatformUtils.service.ts +++ b/electron/src/services/electronPlatformUtils.service.ts @@ -15,11 +15,7 @@ import { ThemeType } from 'jslib-common/enums/themeType'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; - -import { ElectronConstants } from '../electronConstants'; +import { StateService } from 'jslib-common/abstractions/state.service'; export class ElectronPlatformUtilsService implements PlatformUtilsService { identityClientId: string; @@ -27,7 +23,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { private deviceCache: DeviceType = null; constructor(protected i18nService: I18nService, private messagingService: MessagingService, - private isDesktopApp: boolean, private storageService: StorageService) { + private isDesktopApp: boolean, private stateService: StateService) { this.identityClientId = isDesktopApp ? 'desktop' : 'connector'; } @@ -178,8 +174,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(clipboard.readText(type)); } - supportsBiometric(): Promise { - return this.storageService.get(ElectronConstants.enableBiometric); + async supportsBiometric(): Promise { + return await this.stateService.getEnableBiometric(); } authenticateBiometric(): Promise { @@ -200,7 +196,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } async getEffectiveTheme() { - const theme = await this.storageService.get(ConstantsService.themeKey); + const theme = await this.stateService.getTheme(); if (theme == null || theme === ThemeType.System) { return this.getDefaultSystemTheme(); } else { diff --git a/electron/src/services/electronRendererSecureStorage.service.ts b/electron/src/services/electronRendererSecureStorage.service.ts index fb71805811..13a7459a27 100644 --- a/electron/src/services/electronRendererSecureStorage.service.ts +++ b/electron/src/services/electronRendererSecureStorage.service.ts @@ -1,9 +1,11 @@ import { ipcRenderer } from 'electron'; -import { StorageService, StorageServiceOptions } from 'jslib-common/abstractions/storage.service'; +import { StorageService } from 'jslib-common/abstractions/storage.service'; + +import { StorageOptions } from 'jslib-common/models/domain/storageOptions'; export class ElectronRendererSecureStorageService implements StorageService { - async get(key: string, options?: StorageServiceOptions): Promise { + async get(key: string, options?: StorageOptions): Promise { const val = ipcRenderer.sendSync('keytar', { action: 'getPassword', key: key, @@ -12,7 +14,7 @@ export class ElectronRendererSecureStorageService implements StorageService { return Promise.resolve(val != null ? JSON.parse(val) as T : null); } - async has(key: string, options?: StorageServiceOptions): Promise { + async has(key: string, options?: StorageOptions): Promise { const val = ipcRenderer.sendSync('keytar', { action: 'hasPassword', key: key, @@ -21,7 +23,7 @@ export class ElectronRendererSecureStorageService implements StorageService { return Promise.resolve(!!val); } - async save(key: string, obj: any, options?: StorageServiceOptions): Promise { + async save(key: string, obj: any, options?: StorageOptions): Promise { ipcRenderer.sendSync('keytar', { action: 'setPassword', key: key, @@ -31,7 +33,7 @@ export class ElectronRendererSecureStorageService implements StorageService { return Promise.resolve(); } - async remove(key: string, options?: StorageServiceOptions): Promise { + async remove(key: string, options?: StorageOptions): Promise { ipcRenderer.sendSync('keytar', { action: 'deletePassword', key: key, diff --git a/electron/src/tray.main.ts b/electron/src/tray.main.ts index 30df8410db..c75089044d 100644 --- a/electron/src/tray.main.ts +++ b/electron/src/tray.main.ts @@ -2,7 +2,6 @@ import { app, BrowserWindow, Menu, - MenuItem, MenuItemConstructorOptions, nativeImage, Tray, @@ -10,9 +9,8 @@ import { import * as path from 'path'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; -import { ElectronConstants } from './electronConstants'; import { WindowMain } from './window.main'; export class TrayMain { @@ -24,7 +22,7 @@ export class TrayMain { private pressedIcon: Electron.NativeImage; constructor(private windowMain: WindowMain, private i18nService: I18nService, - private storageService: StorageService) { + private stateService: StateService) { if (process.platform === 'win32') { this.icon = path.join(__dirname, '/images/icon.ico'); } else if (process.platform === 'darwin') { @@ -55,21 +53,21 @@ export class TrayMain { } this.contextMenu = Menu.buildFromTemplate(menuItemOptions); - if (await this.storageService.get(ElectronConstants.enableTrayKey)) { + if (await this.stateService.getEnableTray()) { this.showTray(); } } setupWindowListeners(win: BrowserWindow) { win.on('minimize', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { + if (await this.stateService.getEnableMinimizeToTray()) { e.preventDefault(); this.hideToTray(); } }); win.on('close', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { + if (await this.stateService.getEnableCloseToTray()) { if (!this.windowMain.isQuitting) { e.preventDefault(); this.hideToTray(); @@ -78,7 +76,7 @@ export class TrayMain { }); win.on('show', async (e: Event) => { - const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); + const enableTray = await this.stateService.getEnableTray(); if (!enableTray) { setTimeout(() => this.removeTray(false), 100); } @@ -103,7 +101,7 @@ export class TrayMain { if (this.windowMain.win != null) { this.windowMain.win.hide(); } - if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { + if (this.isDarwin() && !await this.stateService.getAlwaysShowDock()) { this.hideDock(); } } @@ -167,7 +165,7 @@ export class TrayMain { } if (this.windowMain.win.isVisible()) { this.windowMain.win.hide(); - if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { + if (this.isDarwin() && !await this.stateService.getAlwaysShowDock()) { this.hideDock(); } } else { diff --git a/electron/src/utils.ts b/electron/src/utils.ts index ce4de6058d..7a33142f80 100644 --- a/electron/src/utils.ts +++ b/electron/src/utils.ts @@ -25,8 +25,12 @@ export function isAppImage() { return process.platform === 'linux' && 'APPIMAGE' in process.env; } +export function isMac() { + return process.platform === 'darwin'; +} + export function isMacAppStore() { - return process.platform === 'darwin' && process.mas && process.mas === true; + return isMac() && process.mas && process.mas === true; } export function isWindowsStore() { diff --git a/electron/src/window.main.ts b/electron/src/window.main.ts index f51dc11410..07cc5166b8 100644 --- a/electron/src/window.main.ts +++ b/electron/src/window.main.ts @@ -7,9 +7,8 @@ import * as path from 'path'; import * as url from 'url'; import { LogService } from 'jslib-common/abstractions/log.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; -import { ElectronConstants } from './electronConstants'; import { cleanUserAgent, isDev, @@ -17,11 +16,8 @@ import { isSnapStore, } from './utils'; +const mainWindowSizeKey = 'mainWindowSize'; const WindowEventHandlingDelay = 100; -const Keys = { - mainWindowSize: 'mainWindowSize', -}; - export class WindowMain { win: BrowserWindow; isQuitting: boolean = false; @@ -30,7 +26,7 @@ export class WindowMain { private windowStates: { [key: string]: any; } = {}; private enableAlwaysOnTop: boolean = false; - constructor(private storageService: StorageService, private logService: LogService, + constructor(private stateService: StateService, private logService: LogService, private hideTitleBar = false, private defaultWidth = 950, private defaultHeight = 600, private argvCallback: (argv: string[]) => void = null, private createWindowCallback: (win: BrowserWindow) => void) { } @@ -107,18 +103,18 @@ export class WindowMain { } async createWindow(): Promise { - this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, this.defaultWidth, + this.windowStates[mainWindowSizeKey] = await this.getWindowState(mainWindowSizeKey, this.defaultWidth, this.defaultHeight); - this.enableAlwaysOnTop = await this.storageService.get(ElectronConstants.enableAlwaysOnTopKey); + this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop(); // Create the browser window. this.win = new BrowserWindow({ - width: this.windowStates[Keys.mainWindowSize].width, - height: this.windowStates[Keys.mainWindowSize].height, + width: this.windowStates[mainWindowSizeKey].width, + height: this.windowStates[mainWindowSizeKey].height, minWidth: 680, minHeight: 500, - x: this.windowStates[Keys.mainWindowSize].x, - y: this.windowStates[Keys.mainWindowSize].y, + x: this.windowStates[mainWindowSizeKey].x, + y: this.windowStates[mainWindowSizeKey].y, title: app.name, icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, @@ -132,7 +128,7 @@ export class WindowMain { }, }); - if (this.windowStates[Keys.mainWindowSize].isMaximized) { + if (this.windowStates[mainWindowSizeKey].isMaximized) { this.win.maximize(); } @@ -158,7 +154,7 @@ export class WindowMain { // Emitted when the window is closed. this.win.on('closed', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); + await this.updateWindowState(mainWindowSizeKey, this.win); // Dereference the window object, usually you would store window // in an array if your app supports multi windows, this is the time @@ -167,23 +163,23 @@ export class WindowMain { }); this.win.on('close', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); + await this.updateWindowState(mainWindowSizeKey, this.win); }); this.win.on('maximize', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); + await this.updateWindowState(mainWindowSizeKey, this.win); }); this.win.on('unmaximize', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); + await this.updateWindowState(mainWindowSizeKey, this.win); }); this.win.on('resize', () => { - this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + this.windowStateChangeHandler(mainWindowSizeKey, this.win); }); this.win.on('move', () => { - this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + this.windowStateChangeHandler(mainWindowSizeKey, this.win); }); this.win.on('focus', () => { this.win.webContents.send('messagingService', { @@ -200,7 +196,7 @@ export class WindowMain { async toggleAlwaysOnTop() { this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); this.win.setAlwaysOnTop(this.enableAlwaysOnTop); - await this.storageService.save(ElectronConstants.enableAlwaysOnTopKey, this.enableAlwaysOnTop); + await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop); } private windowStateChangeHandler(configKey: string, win: BrowserWindow) { @@ -219,7 +215,7 @@ export class WindowMain { const bounds = win.getBounds(); if (this.windowStates[configKey] == null) { - this.windowStates[configKey] = await this.storageService.get(configKey); + this.windowStates[configKey] = (await this.stateService.getWindow()).get(configKey); if (this.windowStates[configKey] == null) { this.windowStates[configKey] = {}; } @@ -235,14 +231,19 @@ export class WindowMain { this.windowStates[configKey].height = bounds.height; } - await this.storageService.save(configKey, this.windowStates[configKey]); + const cachedWindow = await this.stateService.getWindow() ?? new Map(); + cachedWindow.set(configKey, this.windowStates[configKey]); + await this.stateService.setWindow(cachedWindow); } catch (e) { this.logService.error(e); } } private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) { - let state = await this.storageService.get(configKey); + const windowState = await this.stateService.getWindow() ?? new Map(); + let state = windowState.has(configKey) ? + windowState.get(configKey) : + null; const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); let displayBounds: Electron.Rectangle = null; diff --git a/node/src/cli/baseProgram.ts b/node/src/cli/baseProgram.ts index 7772ba805b..99447db8cd 100644 --- a/node/src/cli/baseProgram.ts +++ b/node/src/cli/baseProgram.ts @@ -1,15 +1,15 @@ import * as chalk from 'chalk'; +import { StateService } from 'jslib-common/abstractions/state.service'; + import { Response } from './models/response'; import { ListResponse } from './models/response/listResponse'; import { MessageResponse } from './models/response/messageResponse'; import { StringResponse } from './models/response/stringResponse'; -import { UserService } from 'jslib-common/abstractions/user.service'; - export abstract class BaseProgram { constructor( - private userService: UserService, + private stateService: StateService, private writeLn: (s: string, finalLine: boolean, error: boolean) => void) { } protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) { @@ -92,15 +92,15 @@ export abstract class BaseProgram { } protected async exitIfAuthed() { - const authed = await this.userService.isAuthenticated(); + const authed = await this.stateService.getIsAuthenticated(); if (authed) { - const email = await this.userService.getEmail(); + const email = await this.stateService.getEmail(); this.processResponse(Response.error('You are already logged in as ' + email + '.'), true); } } protected async exitIfNotAuthed() { - const authed = await this.userService.isAuthenticated(); + const authed = await this.stateService.getIsAuthenticated(); if (!authed) { this.processResponse(Response.error('You are not logged in.'), true); } diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 7cdb607eb0..0bb744fdd6 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -18,8 +18,8 @@ import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.serv import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { Response } from '../models/response'; @@ -49,7 +49,7 @@ export class LoginCommand { protected i18nService: I18nService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService, - protected userService: UserService, protected cryptoService: CryptoService, + protected stateService: StateService, protected cryptoService: CryptoService, protected policyService: PolicyService, clientId: string, private syncService: SyncService, protected keyConnectorService: KeyConnectorService) { this.clientId = clientId; @@ -300,7 +300,7 @@ export class LoginCommand { } if (this.email == null || this.email === 'undefined') { - this.email = await this.userService.getEmail(); + this.email = await this.stateService.getEmail(); } // Get New Master Password @@ -350,8 +350,8 @@ export class LoginCommand { // Retrieve details for key generation const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); if (enforcedPolicyOptions != null && !this.policyService.evaluateMasterPassword( diff --git a/spec/common/services/cipher.service.spec.ts b/spec/common/services/cipher.service.spec.ts index 83dce24c02..2fe54c61a8 100644 --- a/spec/common/services/cipher.service.spec.ts +++ b/spec/common/services/cipher.service.spec.ts @@ -7,8 +7,8 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { SearchService } from 'jslib-common/abstractions/search.service'; import { SettingsService } from 'jslib-common/abstractions/settings.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; + import { Utils } from 'jslib-common/misc/utils'; import { Cipher } from 'jslib-common/models/domain/cipher'; import { EncArrayBuffer } from 'jslib-common/models/domain/encArrayBuffer'; @@ -22,11 +22,10 @@ const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT) describe('Cipher Service', () => { let cryptoService: SubstituteOf; - let userService: SubstituteOf; + let stateService: SubstituteOf; let settingsService: SubstituteOf; let apiService: SubstituteOf; let fileUploadService: SubstituteOf; - let storageService: SubstituteOf; let i18nService: SubstituteOf; let searchService: SubstituteOf; let logService: SubstituteOf; @@ -35,11 +34,10 @@ describe('Cipher Service', () => { beforeEach(() => { cryptoService = Substitute.for(); - userService = Substitute.for(); + stateService = Substitute.for(); settingsService = Substitute.for(); apiService = Substitute.for(); fileUploadService = Substitute.for(); - storageService = Substitute.for(); i18nService = Substitute.for(); searchService = Substitute.for(); logService = Substitute.for(); @@ -47,12 +45,11 @@ describe('Cipher Service', () => { cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES); cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT)); - cipherService = new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => searchService, logService); + cipherService = new CipherService(cryptoService, settingsService, apiService, fileUploadService, + i18nService, () => searchService, logService, stateService); }); it('attachments upload encrypted file contents', async () => { - const key = new SymmetricCryptoKey(new Uint8Array(32).buffer); const fileName = 'filename'; const fileData = new Uint8Array(10).buffer; cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer));