From 939fd402c390b0d14d33d8f15a2fa7c1772d487b Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:37:25 -0400 Subject: [PATCH] [PM-24677] Slim StateService down so it can be moved to state lib (#16021) * Slim StateService down so it can be moved to state lib * Fix accidental import changes * Add `switchAccount` assertion * Needs to use mock --- .../browser/main-context-menu-handler.spec.ts | 14 +- .../browser/main-context-menu-handler.ts | 12 +- .../browser/src/background/main.background.ts | 29 +- .../sync/foreground-sync.service.spec.ts | 6 +- .../platform/sync/foreground-sync.service.ts | 6 +- apps/browser/src/popup/app.component.ts | 6 +- .../src/popup/services/init.service.ts | 4 +- .../src/popup/services/services.module.ts | 3 +- apps/cli/src/base-program.ts | 2 +- apps/cli/src/oss-serve-configurator.ts | 15 +- apps/cli/src/program.ts | 17 +- .../service-container/service-container.ts | 32 +- apps/cli/src/tools/generate.command.ts | 21 +- apps/desktop/src/app/app.component.ts | 3 + apps/desktop/src/app/services/init.service.ts | 4 +- .../key-management/electron-key.service.ts | 2 +- apps/web/src/app/app.component.ts | 3 + .../app/auth/verify-email-token.component.ts | 7 +- apps/web/src/app/core/init.service.ts | 6 +- libs/angular/src/services/injection-tokens.ts | 2 - .../src/services/jslib-services.module.ts | 34 +- .../login-strategies/login.strategy.spec.ts | 20 +- .../common/login-strategies/login.strategy.ts | 14 - .../user-api-login.strategy.spec.ts | 2 +- .../services/vault-timeout.service.spec.ts | 13 +- .../services/vault-timeout.service.ts | 13 +- .../platform/abstractions/state.service.ts | 64 +- .../src/platform/factories/account-factory.ts | 13 - .../factories/global-state-factory.ts | 15 - .../src/platform/factories/state-factory.ts | 29 - .../models/domain/account-keys.spec.ts | 42 -- .../models/domain/account-profile.spec.ts | 9 - .../platform/models/domain/account.spec.ts | 19 - .../src/platform/models/domain/account.ts | 136 ---- .../models/domain/encryption-pair.spec.ts | 41 -- .../src/platform/models/domain/state.spec.ts | 31 - .../src/platform/models/domain/state.ts | 46 -- .../src/platform/services/state.service.ts | 659 ------------------ .../src/platform/sync/core-sync.service.ts | 12 +- .../sync/default-sync.service.spec.ts | 4 - .../src/platform/sync/default-sync.service.ts | 6 +- .../src/vault/services/cipher.service.spec.ts | 5 - .../src/vault/services/cipher.service.ts | 2 - libs/key-management/src/key.service.ts | 2 +- libs/state/src/index.ts | 1 + .../state/src/legacy/default-state.service.ts | 107 +++ .../src/legacy}/global-state.ts | 0 libs/state/src/legacy/index.ts | 2 + libs/state/src/legacy/state.service.ts | 25 + 49 files changed, 286 insertions(+), 1274 deletions(-) delete mode 100644 libs/common/src/platform/factories/account-factory.ts delete mode 100644 libs/common/src/platform/factories/global-state-factory.ts delete mode 100644 libs/common/src/platform/factories/state-factory.ts delete mode 100644 libs/common/src/platform/models/domain/account-keys.spec.ts delete mode 100644 libs/common/src/platform/models/domain/account-profile.spec.ts delete mode 100644 libs/common/src/platform/models/domain/account.spec.ts delete mode 100644 libs/common/src/platform/models/domain/account.ts delete mode 100644 libs/common/src/platform/models/domain/encryption-pair.spec.ts delete mode 100644 libs/common/src/platform/models/domain/state.spec.ts delete mode 100644 libs/common/src/platform/models/domain/state.ts delete mode 100644 libs/common/src/platform/services/state.service.ts create mode 100644 libs/state/src/legacy/default-state.service.ts rename libs/{common/src/platform/models/domain => state/src/legacy}/global-state.ts (100%) create mode 100644 libs/state/src/legacy/index.ts create mode 100644 libs/state/src/legacy/state.service.ts diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 901d6595fc8..1348928b7e9 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -17,7 +18,6 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -67,7 +67,7 @@ const createCipher = (data?: { }; describe("context-menu", () => { - let stateService: MockProxy; + let tokenService: MockProxy; let autofillSettingsService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; @@ -85,7 +85,7 @@ describe("context-menu", () => { let sut: MainContextMenuHandler; beforeEach(() => { - stateService = mock(); + tokenService = mock(); autofillSettingsService = mock(); i18nService = mock(); logService = mock(); @@ -109,7 +109,7 @@ describe("context-menu", () => { i18nService.t.mockImplementation((key) => key); sut = new MainContextMenuHandler( - stateService, + tokenService, autofillSettingsService, i18nService, logService, @@ -276,7 +276,7 @@ describe("context-menu", () => { it("removes menu items that require code injection", async () => { billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); autofillSettingsService.enableContextMenu$ = of(true); - stateService.getIsAuthenticated.mockResolvedValue(true); + tokenService.hasAccessToken$.mockReturnValue(of(true)); const optionId = "1"; await sut.loadOptions("TEST_TITLE", optionId, createCipher()); @@ -317,7 +317,7 @@ describe("context-menu", () => { }); it("Loads context menu items that ask the user to unlock their vault if they are authed", async () => { - stateService.getIsAuthenticated.mockResolvedValue(true); + tokenService.hasAccessToken$.mockReturnValue(of(true)); await sut.noAccess(); @@ -325,7 +325,7 @@ describe("context-menu", () => { }); it("Loads context menu items that ask the user to login to their vault if they are not authed", async () => { - stateService.getIsAuthenticated.mockResolvedValue(false); + tokenService.hasAccessToken$.mockReturnValue(of(false)); await sut.noAccess(); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index abfa2465c51..00ff55f5517 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -23,7 +24,6 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -152,7 +152,7 @@ export class MainContextMenuHandler { ]; constructor( - private stateService: StateService, + private tokenService: TokenService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private logService: LogService, @@ -343,7 +343,11 @@ export class MainContextMenuHandler { async noAccess() { if (await this.init()) { - const authed = await this.stateService.getIsAuthenticated(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const authed = + userId != null && (await firstValueFrom(this.tokenService.hasAccessToken$(userId))); this.loadOptions( this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"), NOOP_COMMAND_SUFFIX, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ae21b9d2c9b..1558d9926fa 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -111,14 +111,11 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { IpcService } from "@bitwarden/common/platform/ipc"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; // eslint-disable-next-line no-restricted-imports -- Needed for service creation @@ -143,11 +140,11 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ActiveUserStateProvider, + DefaultStateService, DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, @@ -387,6 +384,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; + migrationRunner: MigrationRunner; taskSchedulerService: BrowserTaskSchedulerService; fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; @@ -592,8 +590,9 @@ export default class MainBackground { this.globalStateProvider, this.singleUserStateProvider, ); + const activeUserAccessor = new DefaultActiveUserAccessor(this.accountService); this.activeUserStateProvider = new DefaultActiveUserStateProvider( - new DefaultActiveUserAccessor(this.accountService), + activeUserAccessor, this.singleUserStateProvider, ); this.derivedStateProvider = new InlineDerivedStateProvider(); @@ -639,23 +638,17 @@ export default class MainBackground { this.taskSchedulerService, ); - const migrationRunner = new MigrationRunner( + this.migrationRunner = new MigrationRunner( this.storageService, this.logService, new MigrationBuilderService(), ClientType.Browser, ); - this.stateService = new StateService( + this.stateService = new DefaultStateService( this.storageService, this.secureStorageService, - this.memoryStorageService, - this.logService, - new StateFactory(GlobalState, Account), - this.accountService, - this.environmentService, - this.tokenService, - migrationRunner, + activeUserAccessor, ); this.masterPasswordService = new MasterPasswordService( @@ -887,7 +880,6 @@ export default class MainBackground { this.apiService, this.i18nService, this.searchService, - this.stateService, this.autofillSettingsService, this.encryptService, this.cipherFileUploadService, @@ -946,6 +938,7 @@ export default class MainBackground { this.messagingService, this.searchService, this.stateService, + this.tokenService, this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, @@ -989,7 +982,6 @@ export default class MainBackground { this.sendService, this.logService, this.keyConnectorService, - this.stateService, this.providerService, this.folderApiService, this.organizationService, @@ -1320,7 +1312,7 @@ export default class MainBackground { ); this.mainContextMenuHandler = new MainContextMenuHandler( - this.stateService, + this.tokenService, this.autofillSettingsService, this.i18nService, this.logService, @@ -1387,7 +1379,7 @@ export default class MainBackground { await this.sdkLoadService.loadAndInit(); // Only the "true" background should run migrations - await this.stateService.init({ runMigrations: true }); + await this.migrationRunner.run(); // This is here instead of in in the InitService b/c we don't plan for // side effects to run in the Browser InitService. @@ -1607,6 +1599,7 @@ export default class MainBackground { const needStorageReseed = await this.needsStorageReseed(userBeingLoggedOut); await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.tokenService.clearAccessToken(userBeingLoggedOut); await this.accountService.clean(userBeingLoggedOut); await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); diff --git a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index 34ee4fa0f77..f8b4050a5ce 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -4,8 +4,8 @@ import { Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; @@ -22,7 +22,7 @@ import { FullSyncFinishedMessage } from "./sync-service.listener"; describe("ForegroundSyncService", () => { const userId = Utils.newGuid() as UserId; - const stateService = mock(); + const tokenService = mock(); const folderService = mock(); const folderApiService = mock(); const messageSender = mock(); @@ -38,7 +38,7 @@ describe("ForegroundSyncService", () => { const stateProvider = new FakeStateProvider(accountService); const sut = new ForegroundSyncService( - stateService, + tokenService, folderService, folderApiService, messageSender, diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index 2ac75bbec2c..01b1f35239b 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -4,8 +4,8 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CommandDefinition, MessageListener, @@ -31,7 +31,7 @@ export const DO_FULL_SYNC = new CommandDefinition("doFullSync") export class ForegroundSyncService extends CoreSyncService { constructor( - stateService: StateService, + tokenService: TokenService, folderService: InternalFolderService, folderApiService: FolderApiServiceAbstraction, messageSender: MessageSender, @@ -47,7 +47,7 @@ export class ForegroundSyncService extends CoreSyncService { stateProvider: StateProvider, ) { super( - stateService, + tokenService, folderService, folderApiService, messageSender, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 6a26476de43..fa1e6c237c9 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -28,13 +28,13 @@ import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -102,7 +102,7 @@ export class AppComponent implements OnInit, OnDestroy { private authService: AuthService, private i18nService: I18nService, private router: Router, - private stateService: StateService, + private readonly tokenService: TokenService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, @@ -321,7 +321,7 @@ export class AppComponent implements OnInit, OnDestroy { } private async clearComponentStates() { - if (!(await this.stateService.getIsAuthenticated())) { + if (!(await firstValueFrom(this.tokenService.hasAccessToken$(this.activeUserId)))) { return; } diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 9e750ae7341..1930dbd1d4b 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -8,6 +8,7 @@ import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/browser/browser-popup-utils"; @@ -27,13 +28,14 @@ export class InitService { private themingService: AbstractThemingService, private sdkLoadService: SdkLoadService, private viewCacheService: PopupViewCacheService, + private readonly migrationRunner: MigrationRunner, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.sdkLoadService.loadAndInit(); - await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations + await this.migrationRunner.waitForCompletion(); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); await this.viewCacheService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 76d61eac90d..f531ebd5ca7 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -47,6 +47,7 @@ import { import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -333,7 +334,7 @@ const safeProviders: SafeProvider[] = [ provide: SyncService, useClass: ForegroundSyncService, deps: [ - StateService, + TokenService, InternalFolderService, FolderApiServiceAbstraction, MessageSender, diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index 5719f78c1b9..5957f08de89 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -129,7 +129,7 @@ export abstract class BaseProgram { if (!userId) { fail(); } - const authed = await this.serviceContainer.stateService.getIsAuthenticated({ userId }); + const authed = await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId)); if (!authed) { fail(); } diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index df46e22f84d..6ae2776eae7 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -107,7 +107,8 @@ export class OssServeConfigurator { ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, - this.serviceContainer.stateService, + this.serviceContainer.tokenService, + this.serviceContainer.accountService, ); this.syncCommand = new SyncCommand(this.serviceContainer.syncService); this.statusCommand = new StatusCommand( @@ -417,14 +418,18 @@ export class OssServeConfigurator { } protected async errorIfLocked(res: koa.Response) { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + const userId = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + const authed = + userId != null || + (await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId))); + if (!authed) { this.processResponse(res, Response.error("You are not logged in.")); return true; } - const userId = await firstValueFrom( - this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); if (await this.serviceContainer.keyService.hasUserKey(userId)) { return false; } diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 468901282b4..4d541739aab 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import * as chalk from "chalk"; import { program, Command, OptionValues } from "commander"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of, switchMap } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -129,7 +129,17 @@ export class Program extends BaseProgram { "Path to a file containing your password as its first line", ) .option("--check", "Check login status.", async () => { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + const authed = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return of(false); + } + + return this.serviceContainer.tokenService.hasAccessToken$(account.id); + }), + ), + ); if (authed) { const res = new MessageResponse("You are logged in!", null); this.processResponse(Response.success(res), true); @@ -350,7 +360,8 @@ export class Program extends BaseProgram { .action(async (options) => { const command = new GenerateCommand( this.serviceContainer.passwordGenerationService, - this.serviceContainer.stateService, + this.serviceContainer.tokenService, + this.serviceContainer.accountService, ); const response = await command.run(options); this.processResponse(response); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d93e9838868..e82ceb5a6e9 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -88,10 +88,7 @@ import { import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { MessageSender } from "@bitwarden/common/platform/messaging"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { TaskSchedulerService, DefaultTaskSchedulerService, @@ -108,16 +105,17 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ActiveUserStateProvider, + DefaultStateService, DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, StateEventRunnerService, StateProvider, + StateService, } from "@bitwarden/common/platform/state"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -212,6 +210,7 @@ export class ServiceContainer { secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; + migrationRunner: MigrationRunner; i18nService: I18nService; platformUtilsService: CliPlatformUtilsService; keyService: KeyService; @@ -379,8 +378,10 @@ export class ServiceContainer { this.singleUserStateProvider, ); + const activeUserAccessor = new DefaultActiveUserAccessor(this.accountService); + this.activeUserStateProvider = new DefaultActiveUserStateProvider( - new DefaultActiveUserAccessor(this.accountService), + activeUserAccessor, this.singleUserStateProvider, ); @@ -412,23 +413,17 @@ export class ServiceContainer { logoutCallback, ); - const migrationRunner = new MigrationRunner( + this.migrationRunner = new MigrationRunner( this.storageService, this.logService, new MigrationBuilderService(), ClientType.Cli, ); - this.stateService = new StateService( + this.stateService = new DefaultStateService( this.storageService, this.secureStorageService, - this.memoryStorageService, - this.logService, - new StateFactory(GlobalState, Account), - this.accountService, - this.environmentService, - this.tokenService, - migrationRunner, + activeUserAccessor, ); this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); @@ -713,7 +708,6 @@ export class ServiceContainer { this.apiService, this.i18nService, this.searchService, - this.stateService, this.autofillSettingsService, this.encryptService, this.cipherFileUploadService, @@ -764,6 +758,7 @@ export class ServiceContainer { this.messagingService, this.searchService, this.stateService, + this.tokenService, this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, @@ -790,7 +785,6 @@ export class ServiceContainer { this.sendService, this.logService, this.keyConnectorService, - this.stateService, this.providerService, this.folderApiService, this.organizationService, @@ -903,7 +897,8 @@ export class ServiceContainer { await this.stateEventRunnerService.handleEvent("logout", userId as UserId); - await this.stateService.clean(); + await this.stateService.clean({ userId: userId }); + await this.tokenService.clearAccessToken(userId); await this.accountService.clean(userId as UserId); await this.accountService.switchAccount(null); process.env.BW_SESSION = undefined; @@ -917,7 +912,8 @@ export class ServiceContainer { await this.sdkLoadService.loadAndInit(); await this.storageService.init(); - await this.stateService.init(); + + await this.migrationRunner.run(); this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/cli/src/tools/generate.command.ts b/apps/cli/src/tools/generate.command.ts index 64c6118a0c6..1d8a8690ab3 100644 --- a/apps/cli/src/tools/generate.command.ts +++ b/apps/cli/src/tools/generate.command.ts @@ -1,6 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { firstValueFrom, of, switchMap } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { DefaultPasswordGenerationOptions, DefaultPassphraseGenerationOptions, @@ -17,7 +20,8 @@ import { CliUtils } from "../utils"; export class GenerateCommand { constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, - private stateService: StateService, + private tokenService: TokenService, + private accountService: AccountService, ) {} async run(cmdOptions: Record): Promise { @@ -38,7 +42,18 @@ export class GenerateCommand { ambiguous: !normalizedOptions.ambiguous, }; - const enforcedOptions = (await this.stateService.getIsAuthenticated()) + const shouldEnforceOptions = await firstValueFrom( + this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return of(false); + } + + return this.tokenService.hasAccessToken$(account.id); + }), + ), + ); + const enforcedOptions = shouldEnforceOptions ? (await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options))[0] : options; diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b0c5eb03723..04651ed0c10 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -40,6 +40,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -175,6 +176,7 @@ export class AppComponent implements OnInit, OnDestroy { private readonly destroyRef: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, private restrictedItemTypesService: RestrictedItemTypesService, + private readonly tokenService: TokenService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -684,6 +686,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.tokenService.clearAccessToken(userBeingLoggedOut); await this.accountService.clean(userBeingLoggedOut); // HACK: Wait for the user logging outs authentication status to transition to LoggedOut diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 698427c1e57..2c68821b6c7 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -15,6 +15,7 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk- import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; @@ -52,6 +53,7 @@ export class InitService { private autotypeService: DesktopAutotypeService, private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, + private readonly migrationRunner: MigrationRunner, ) {} init() { @@ -59,7 +61,7 @@ export class InitService { await this.sdkLoadService.loadAndInit(); await this.sshAgentService.init(); this.nativeMessagingService.init(); - await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process const accounts = await firstValueFrom(this.accountService.accounts$); const setUserKeyInMemoryPromises = []; diff --git a/apps/desktop/src/key-management/electron-key.service.ts b/apps/desktop/src/key-management/electron-key.service.ts index 48ccd3f27fd..562662f6696 100644 --- a/apps/desktop/src/key-management/electron-key.service.ts +++ b/apps/desktop/src/key-management/electron-key.service.ts @@ -65,7 +65,7 @@ export class ElectronKeyService extends DefaultKeyService { protected override async getKeyFromStorage( keySuffix: KeySuffixOptions, - userId?: UserId, + userId: UserId, ): Promise { return await super.getKeyFromStorage(keySuffix, userId); } diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 694d0c6eb9a..ae20670c2dd 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -12,6 +12,7 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; @@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit { private deviceTrustToastService: DeviceTrustToastService, private readonly destoryRef: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, + private readonly tokenService: TokenService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -297,6 +299,7 @@ export class AppComponent implements OnDestroy, OnInit { await this.searchService.clearIndex(userId); this.authService.logOut(async () => { await this.stateService.clean({ userId: userId }); + await this.tokenService.clearAccessToken(userId); await this.accountService.clean(userId); await this.accountService.switchAccount(null); diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 9e44cc7a713..2c4fa7f447c 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -2,14 +2,15 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { VerifyEmailRequest } from "@bitwarden/common/models/request/verify-email.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ToastService } from "@bitwarden/components"; @Component({ @@ -25,7 +26,7 @@ export class VerifyEmailTokenComponent implements OnInit { private route: ActivatedRoute, private apiService: ApiService, private logService: LogService, - private stateService: StateService, + private tokenService: TokenService, private toastService: ToastService, ) {} @@ -37,7 +38,7 @@ export class VerifyEmailTokenComponent implements OnInit { await this.apiService.postAccountVerifyEmailToken( new VerifyEmailRequest(qParams.userId, qParams.token), ); - if (await this.stateService.getIsAuthenticated()) { + if (await firstValueFrom(this.tokenService.hasAccessToken$(qParams.userId))) { await this.apiService.refreshIdentityToken(); } this.toastService.showToast({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index f4d05171d56..57d9918aad7 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -11,10 +11,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; -import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { IpcService } from "@bitwarden/common/platform/ipc"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { TaskService } from "@bitwarden/common/vault/tasks"; @@ -31,7 +31,6 @@ export class InitService { private i18nService: I18nServiceAbstraction, private eventUploadService: EventUploadServiceAbstraction, private twoFactorService: TwoFactorServiceAbstraction, - private stateService: StateServiceAbstraction, private keyService: KeyServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, @@ -41,13 +40,14 @@ export class InitService { private ipcService: IpcService, private sdkLoadService: SdkLoadService, private taskService: TaskService, + private readonly migrationRunner: MigrationRunner, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.sdkLoadService.loadAndInit(); - await this.stateService.init(); + await this.migrationRunner.run(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); if (activeAccount) { diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 2122506890a..6bf3ab77252 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -13,7 +13,6 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Theme } from "@bitwarden/common/platform/enums"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; import { HttpOperations } from "@bitwarden/common/services/api.service"; import { SafeInjectionToken } from "@bitwarden/ui-common"; @@ -33,7 +32,6 @@ export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken< >("OBSERVABLE_DISK_LOCAL_STORAGE"); export const MEMORY_STORAGE = new SafeInjectionToken("MEMORY_STORAGE"); export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); -export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); export const LOGOUT_CALLBACK = new SafeInjectionToken< (logoutReason: LogoutReason, userId?: string) => Promise >("LOGOUT_CALLBACK"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3a6e7439ccc..d6e4e901b50 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -196,13 +196,10 @@ import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.serv import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; // eslint-disable-next-line no-restricted-imports -- Needed for service creation import { @@ -228,13 +225,13 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { ActiveUserAccessor, ActiveUserStateProvider, + DefaultStateService, DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, @@ -371,12 +368,10 @@ import { LOCKED_CALLBACK, LOG_MAC_FAILURES, LOGOUT_CALLBACK, - MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, SECURE_STORAGE, - STATE_FACTORY, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, @@ -414,10 +409,6 @@ const safeProviders: SafeProvider[] = [ useFactory: (window: Window) => window.navigator.language, deps: [WINDOW], }), - safeProvider({ - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }), // TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService safeProvider({ provide: LOGOUT_CALLBACK, @@ -530,7 +521,6 @@ const safeProviders: SafeProvider[] = [ apiService: ApiServiceAbstraction, i18nService: I18nServiceAbstraction, searchService: SearchServiceAbstraction, - stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, @@ -547,7 +537,6 @@ const safeProviders: SafeProvider[] = [ apiService, i18nService, searchService, - stateService, autofillSettingsService, encryptService, fileUploadService, @@ -564,7 +553,6 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, I18nServiceAbstraction, SearchServiceAbstraction, - StateServiceAbstraction, AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, @@ -801,7 +789,6 @@ const safeProviders: SafeProvider[] = [ InternalSendService, LogService, KeyConnectorServiceAbstraction, - StateServiceAbstraction, ProviderServiceAbstraction, FolderApiServiceAbstraction, InternalOrganizationServiceAbstraction, @@ -849,6 +836,7 @@ const safeProviders: SafeProvider[] = [ MessagingServiceAbstraction, SearchServiceAbstraction, StateServiceAbstraction, + TokenServiceAbstraction, AuthServiceAbstraction, VaultTimeoutSettingsService, StateEventRunnerService, @@ -868,24 +856,10 @@ const safeProviders: SafeProvider[] = [ useClass: SsoLoginService, deps: [StateProvider, LogService], }), - safeProvider({ - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }), safeProvider({ provide: StateServiceAbstraction, - useClass: StateService, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - STATE_FACTORY, - AccountServiceAbstraction, - EnvironmentService, - TokenServiceAbstraction, - MigrationRunner, - ], + useClass: DefaultStateService, + deps: [AbstractStorageService, SECURE_STORAGE, ActiveUserAccessor], }), safeProvider({ provide: IndividualVaultExportServiceAbstraction, diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 1a6592887ba..222fb5592aa 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -32,7 +32,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { @@ -243,18 +242,8 @@ describe("LoginStrategy", () => { refreshToken, ); - expect(stateService.addAccount).toHaveBeenCalledWith( - new Account({ - profile: { - ...new AccountProfile(), - ...{ - userId: userId, - name: name, - email: email, - }, - }, - }), - ); + expect(environmentService.seedUserEnvironment).toHaveBeenCalled(); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( UserDecryptionOptions.fromResponse(idTokenResponse), ); @@ -388,7 +377,8 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(stateService.addAccount).not.toHaveBeenCalled(); + expect(environmentService.seedUserEnvironment).not.toHaveBeenCalled(); + expect(accountService.mock.switchAccount).not.toHaveBeenCalled(); expect(messagingService.send).not.toHaveBeenCalled(); expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); @@ -422,7 +412,7 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(stateService.addAccount).not.toHaveBeenCalled(); + expect(environmentService.seedUserEnvironment).not.toHaveBeenCalled(); expect(messagingService.send).not.toHaveBeenCalled(); const expected = new AuthResult(); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 53e34147d9f..4c7a38254d7 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -32,7 +32,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; -import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { KeyService, @@ -198,19 +197,6 @@ export abstract class LoginStrategy { await this.accountService.switchAccount(userId); - await this.stateService.addAccount( - new Account({ - profile: { - ...new AccountProfile(), - ...{ - userId, - name: accountInformation.name, - email: accountInformation.email, - }, - }, - }), - ); - await this.verifyAccountAdded(userId); // We must set user decryption options before retrieving vault timeout settings diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 7114afbf94f..a6446401f70 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -170,7 +170,7 @@ describe("UserApiLoginStrategy", () => { mockVaultTimeoutAction, mockVaultTimeout, ); - expect(stateService.addAccount).toHaveBeenCalled(); + expect(environmentService.seedUserEnvironment).toHaveBeenCalled(); }); it("sets the encrypted user key and private key from the identity token response", async () => { diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts index 9963e7d24f8..26d263d7e72 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts @@ -12,15 +12,16 @@ import { LogoutReason } from "@bitwarden/auth/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { BiometricsService } from "@bitwarden/key-management"; +import { StateService } from "@bitwarden/state"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { AccountInfo } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { LogService } from "../../../platform/abstractions/log.service"; import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; import { TaskSchedulerService } from "../../../platform/scheduling"; import { StateEventRunnerService } from "../../../platform/state"; @@ -45,6 +46,7 @@ describe("VaultTimeoutService", () => { let messagingService: MockProxy; let searchService: MockProxy; let stateService: MockProxy; + let tokenService: MockProxy; let authService: MockProxy; let vaultTimeoutSettingsService: MockProxy; let stateEventRunnerService: MockProxy; @@ -71,6 +73,7 @@ describe("VaultTimeoutService", () => { messagingService = mock(); searchService = mock(); stateService = mock(); + tokenService = mock(); authService = mock(); vaultTimeoutSettingsService = mock(); stateEventRunnerService = mock(); @@ -99,6 +102,7 @@ describe("VaultTimeoutService", () => { messagingService, searchService, stateService, + tokenService, authService, vaultTimeoutSettingsService, stateEventRunnerService, @@ -141,9 +145,8 @@ describe("VaultTimeoutService", () => { authService.getAuthStatus.mockImplementation((userId) => { return Promise.resolve(accounts[userId]?.authStatus); }); - stateService.getIsAuthenticated.mockImplementation((options) => { - // Just like actual state service, if no userId is given fallback to active userId - return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated); + tokenService.hasAccessToken$.mockImplementation((userId) => { + return of(accounts[userId]?.isAuthenticated ?? false); }); vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation((userId) => { @@ -201,7 +204,7 @@ describe("VaultTimeoutService", () => { const expectUserToHaveLocked = (userId: string) => { // This does NOT assert all the things that the lock process does - expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); + expect(tokenService.hasAccessToken$).toHaveBeenCalledWith(userId); expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index 6d71bad0b0a..98f6f76fbe7 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -14,6 +14,7 @@ import { BiometricsService } from "@bitwarden/key-management"; import { AccountService } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { LogService } from "../../../platform/abstractions/log.service"; import { MessagingService } from "../../../platform/abstractions/messaging.service"; @@ -43,6 +44,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private messagingService: MessagingService, private searchService: SearchService, private stateService: StateService, + private tokenService: TokenService, private authService: AuthService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private stateEventRunnerService: StateEventRunnerService, @@ -108,7 +110,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { async lock(userId?: UserId): Promise { await this.biometricService.setShouldAutopromptNow(false); - const authed = await this.stateService.getIsAuthenticated({ userId: userId }); + const lockingUserId = + userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); + + const authed = await firstValueFrom(this.tokenService.hasAccessToken$(lockingUserId)); if (!authed) { return; } @@ -121,12 +126,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - const lockingUserId = userId ?? currentUserId; - // HACK: Start listening for the transition of the locking user from something to the locked state. // This is very much a hack to ensure that the authentication status to retrievable right after // it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4c1c000284e..612b801d535 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,63 +1 @@ -import { BiometricKey } from "../../auth/types/biometric-key"; -import { Account } from "../models/domain/account"; -import { StorageOptions } from "../models/domain/storage-options"; - -/** - * Options for customizing the initiation behavior. - */ -export type InitOptions = { - /** - * Whether or not to run state migrations as part of the init process. Defaults to true. - * - * If false, the init method will instead wait for migrations to complete before doing its - * other init operations. Make sure migrations have either already completed, or will complete - * before calling {@link StateService.init} with `runMigrations: false`. - */ - runMigrations?: boolean; -}; - -export abstract class StateService { - abstract addAccount(account: T): Promise; - abstract clean(options?: StorageOptions): Promise; - abstract init(initOptions?: InitOptions): Promise; - - /** - * Gets the user's auto key - */ - abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise; - /** - * Sets the user's auto key - */ - abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise; - /** - * Gets the user's biometric key - */ - abstract getUserKeyBiometric(options?: StorageOptions): Promise; - /** - * Checks if the user has a biometric key available - */ - abstract hasUserKeyBiometric(options?: StorageOptions): Promise; - /** - * Sets the user's biometric key - */ - abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise; - /** - * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService - */ - abstract setEnableDuckDuckGoBrowserIntegration( - value: boolean, - options?: StorageOptions, - ): Promise; - abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise; - abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise; - - /** - * @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead. - */ - abstract getIsAuthenticated(options?: StorageOptions): Promise; - - /** - * @deprecated Use `AccountService.activeAccount$` instead. - */ - abstract getUserId(options?: StorageOptions): Promise; -} +export { StateService } from "@bitwarden/state"; diff --git a/libs/common/src/platform/factories/account-factory.ts b/libs/common/src/platform/factories/account-factory.ts deleted file mode 100644 index 1fe5aee369c..00000000000 --- a/libs/common/src/platform/factories/account-factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Account } from "../models/domain/account"; - -export class AccountFactory { - private accountConstructor: new (init: Partial) => T; - - constructor(accountConstructor: new (init: Partial) => T) { - this.accountConstructor = accountConstructor; - } - - create(args: Partial) { - return new this.accountConstructor(args); - } -} diff --git a/libs/common/src/platform/factories/global-state-factory.ts b/libs/common/src/platform/factories/global-state-factory.ts deleted file mode 100644 index b52b022fd18..00000000000 --- a/libs/common/src/platform/factories/global-state-factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { GlobalState } from "../models/domain/global-state"; - -export class GlobalStateFactory { - private globalStateConstructor: new (init: Partial) => T; - - constructor(globalStateConstructor: new (init: Partial) => T) { - this.globalStateConstructor = globalStateConstructor; - } - - create(args?: Partial) { - return new this.globalStateConstructor(args); - } -} diff --git a/libs/common/src/platform/factories/state-factory.ts b/libs/common/src/platform/factories/state-factory.ts deleted file mode 100644 index fcdd3220c2b..00000000000 --- a/libs/common/src/platform/factories/state-factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Account } from "../models/domain/account"; -import { GlobalState } from "../models/domain/global-state"; - -import { AccountFactory } from "./account-factory"; -import { GlobalStateFactory } from "./global-state-factory"; - -export class StateFactory< - TGlobal extends GlobalState = GlobalState, - TAccount extends Account = Account, -> { - private globalStateFactory: GlobalStateFactory; - private accountFactory: AccountFactory; - - constructor( - globalStateConstructor: new (init: Partial) => TGlobal, - accountConstructor: new (init: Partial) => TAccount, - ) { - this.globalStateFactory = new GlobalStateFactory(globalStateConstructor); - this.accountFactory = new AccountFactory(accountConstructor); - } - - createGlobal(args: Partial): TGlobal { - return this.globalStateFactory.create(args); - } - - createAccount(args: Partial): TAccount { - return this.accountFactory.create(args); - } -} diff --git a/libs/common/src/platform/models/domain/account-keys.spec.ts b/libs/common/src/platform/models/domain/account-keys.spec.ts deleted file mode 100644 index 6bdb08edd51..00000000000 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { makeStaticByteArray } from "../../../../spec"; -import { Utils } from "../../misc/utils"; - -import { AccountKeys, EncryptionPair } from "./account"; - -describe("AccountKeys", () => { - describe("toJSON", () => { - it("should serialize itself", () => { - const keys = new AccountKeys(); - const buffer = makeStaticByteArray(64); - keys.publicKey = buffer; - - const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString"); - keys.toJSON(); - expect(bufferSpy).toHaveBeenCalledWith(buffer); - }); - - it("should serialize public key as a string", () => { - const keys = new AccountKeys(); - keys.publicKey = Utils.fromByteStringToArray("hello"); - const json = JSON.stringify(keys); - expect(json).toContain('"publicKey":"hello"'); - }); - }); - - describe("fromJSON", () => { - it("should deserialize public key to a buffer", () => { - const keys = AccountKeys.fromJSON({ - publicKey: "hello", - }); - expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello")); - }); - - it("should deserialize privateKey", () => { - const spy = jest.spyOn(EncryptionPair, "fromJSON"); - AccountKeys.fromJSON({ - privateKey: { encrypted: "encrypted", decrypted: "decrypted" }, - } as any); - expect(spy).toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account-profile.spec.ts b/libs/common/src/platform/models/domain/account-profile.spec.ts deleted file mode 100644 index 7c6deda34eb..00000000000 --- a/libs/common/src/platform/models/domain/account-profile.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountProfile } from "./account"; - -describe("AccountProfile", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountProfile.fromJSON({})).toBeInstanceOf(AccountProfile); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts deleted file mode 100644 index 307fde62f93..00000000000 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Account, AccountKeys, AccountProfile } from "./account"; - -describe("Account", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(Account.fromJSON({})).toBeInstanceOf(Account); - }); - - it("should call all the sub-fromJSONs", () => { - const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); - const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); - - Account.fromJSON({}); - - expect(keysSpy).toHaveBeenCalled(); - expect(profileSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts deleted file mode 100644 index b9d10f47e97..00000000000 --- a/libs/common/src/platform/models/domain/account.ts +++ /dev/null @@ -1,136 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Jsonify } from "type-fest"; - -import { DeepJsonify } from "../../../types/deep-jsonify"; -import { Utils } from "../../misc/utils"; - -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; - -export class EncryptionPair { - encrypted?: TEncrypted; - decrypted?: TDecrypted; - - toJSON() { - return { - encrypted: this.encrypted, - decrypted: - this.decrypted instanceof ArrayBuffer - ? Utils.fromBufferToByteString(this.decrypted) - : this.decrypted, - }; - } - - static fromJSON( - obj: { encrypted?: Jsonify; decrypted?: string | Jsonify }, - decryptedFromJson?: (decObj: Jsonify | string) => TDecrypted, - encryptedFromJson?: (encObj: Jsonify) => TEncrypted, - ) { - if (obj == null) { - return null; - } - - const pair = new EncryptionPair(); - if (obj?.encrypted != null) { - pair.encrypted = encryptedFromJson - ? encryptedFromJson(obj.encrypted) - : (obj.encrypted as TEncrypted); - } - if (obj?.decrypted != null) { - pair.decrypted = decryptedFromJson - ? decryptedFromJson(obj.decrypted) - : (obj.decrypted as TDecrypted); - } - return pair; - } -} - -export class AccountKeys { - publicKey?: Uint8Array; - - /** @deprecated July 2023, left for migration purposes*/ - cryptoSymmetricKey?: EncryptionPair = new EncryptionPair< - string, - SymmetricCryptoKey - >(); - - toJSON() { - // If you pass undefined into fromBufferToByteString, you will get an empty string back - // which will cause all sorts of headaches down the line when you try to getPublicKey - // and expect a Uint8Array and get an empty string instead. - return Utils.merge(this, { - publicKey: this.publicKey ? Utils.fromBufferToByteString(this.publicKey) : undefined, - }); - } - - static fromJSON(obj: DeepJsonify): AccountKeys { - if (obj == null) { - return null; - } - return Object.assign(new AccountKeys(), obj, { - cryptoSymmetricKey: EncryptionPair.fromJSON( - obj?.cryptoSymmetricKey, - SymmetricCryptoKey.fromJSON, - ), - publicKey: Utils.fromByteStringToArray(obj?.publicKey), - }); - } - - static initRecordEncryptionPairsFromJSON(obj: any) { - return EncryptionPair.fromJSON(obj, (decObj: any) => { - if (obj == null) { - return null; - } - - const record: Record = {}; - for (const id in decObj) { - record[id] = SymmetricCryptoKey.fromJSON(decObj[id]); - } - return record; - }); - } -} - -export class AccountProfile { - name?: string; - email?: string; - emailVerified?: boolean; - userId?: string; - - static fromJSON(obj: Jsonify): AccountProfile { - if (obj == null) { - return null; - } - - return Object.assign(new AccountProfile(), obj); - } -} - -export class Account { - keys?: AccountKeys = new AccountKeys(); - profile?: AccountProfile = new AccountProfile(); - - constructor(init: Partial) { - Object.assign(this, { - keys: { - ...new AccountKeys(), - ...init?.keys, - }, - profile: { - ...new AccountProfile(), - ...init?.profile, - }, - }); - } - - static fromJSON(json: Jsonify): Account { - if (json == null) { - return null; - } - - return Object.assign(new Account({}), json, { - keys: AccountKeys.fromJSON(json?.keys), - profile: AccountProfile.fromJSON(json?.profile), - }); - } -} diff --git a/libs/common/src/platform/models/domain/encryption-pair.spec.ts b/libs/common/src/platform/models/domain/encryption-pair.spec.ts deleted file mode 100644 index 1418c125ed6..00000000000 --- a/libs/common/src/platform/models/domain/encryption-pair.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Utils } from "../../misc/utils"; - -import { EncryptionPair } from "./account"; - -describe("EncryptionPair", () => { - describe("toJSON", () => { - it("should populate decryptedSerialized for buffer arrays", () => { - const pair = new EncryptionPair(); - pair.decrypted = Utils.fromByteStringToArray("hello").buffer; - const json = pair.toJSON(); - expect(json.decrypted).toEqual("hello"); - }); - - it("should populate decryptedSerialized for TypesArrays", () => { - const pair = new EncryptionPair(); - pair.decrypted = Utils.fromByteStringToArray("hello"); - const json = pair.toJSON(); - expect(json.decrypted).toEqual(new Uint8Array([104, 101, 108, 108, 111])); - }); - - it("should serialize encrypted and decrypted", () => { - const pair = new EncryptionPair(); - pair.encrypted = "hello"; - pair.decrypted = "world"; - const json = pair.toJSON(); - expect(json.encrypted).toEqual("hello"); - expect(json.decrypted).toEqual("world"); - }); - }); - - describe("fromJSON", () => { - it("should deserialize encrypted and decrypted", () => { - const pair = EncryptionPair.fromJSON({ - encrypted: "hello", - decrypted: "world", - }); - expect(pair.encrypted).toEqual("hello"); - expect(pair.decrypted).toEqual("world"); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/state.spec.ts b/libs/common/src/platform/models/domain/state.spec.ts deleted file mode 100644 index 55d17bded3f..00000000000 --- a/libs/common/src/platform/models/domain/state.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Account } from "./account"; -import { State } from "./state"; - -describe("state", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(State.fromJSON({}, () => new Account({}))).toBeInstanceOf(State); - }); - - it("should always assign an object to accounts", () => { - const state = State.fromJSON({}, () => new Account({})); - expect(state.accounts).not.toBeNull(); - expect(state.accounts).toEqual({}); - }); - - it("should build an account map", () => { - const accountsSpy = jest.spyOn(Account, "fromJSON"); - const state = State.fromJSON( - { - accounts: { - userId: {}, - }, - }, - Account.fromJSON, - ); - - expect(state.accounts["userId"]).toBeInstanceOf(Account); - expect(accountsSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/state.ts b/libs/common/src/platform/models/domain/state.ts deleted file mode 100644 index d9f5849a3ca..00000000000 --- a/libs/common/src/platform/models/domain/state.ts +++ /dev/null @@ -1,46 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Jsonify } from "type-fest"; - -import { Account } from "./account"; -import { GlobalState } from "./global-state"; - -export class State< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account, -> { - accounts: { [userId: string]: TAccount } = {}; - globals: TGlobalState; - - constructor(globals: TGlobalState) { - this.globals = globals; - } - - // TODO, make Jsonify work. It currently doesn't because Globals doesn't implement Jsonify. - static fromJSON( - obj: any, - accountDeserializer: (json: Jsonify) => TAccount, - ): State { - if (obj == null) { - return null; - } - - return Object.assign(new State(null), obj, { - accounts: State.buildAccountMapFromJSON(obj?.accounts, accountDeserializer), - }); - } - - private static buildAccountMapFromJSON( - jsonAccounts: { [userId: string]: Jsonify }, - accountDeserializer: (json: Jsonify) => TAccount, - ) { - if (!jsonAccounts) { - return {}; - } - const accounts: { [userId: string]: TAccount } = {}; - for (const userId in jsonAccounts) { - accounts[userId] = accountDeserializer(jsonAccounts[userId]); - } - return accounts; - } -} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts deleted file mode 100644 index 284c8a7f2dc..00000000000 --- a/libs/common/src/platform/services/state.service.ts +++ /dev/null @@ -1,659 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; -import { Jsonify, JsonValue } from "type-fest"; - -import { AccountService } from "../../auth/abstractions/account.service"; -import { TokenService } from "../../auth/abstractions/token.service"; -import { BiometricKey } from "../../auth/types/biometric-key"; -import { UserId } from "../../types/guid"; -import { EnvironmentService } from "../abstractions/environment.service"; -import { LogService } from "../abstractions/log.service"; -import { - InitOptions, - StateService as StateServiceAbstraction, -} from "../abstractions/state.service"; -import { AbstractStorageService } from "../abstractions/storage.service"; -import { HtmlStorageLocation, StorageLocation } from "../enums"; -import { StateFactory } from "../factories/state-factory"; -import { Account } from "../models/domain/account"; -import { GlobalState } from "../models/domain/global-state"; -import { State } from "../models/domain/state"; -import { StorageOptions } from "../models/domain/storage-options"; - -import { MigrationRunner } from "./migration-runner"; - -const keys = { - state: "state", - stateVersion: "stateVersion", - global: "global", - tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication -}; - -const partialKeys = { - userAutoKey: "_user_auto", - userBiometricKey: "_user_biometric", - - autoKey: "_masterkey_auto", - masterKey: "_masterkey", -}; - -const DDG_SHARED_KEY = "DuckDuckGoSharedKey"; - -export class StateService< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account, -> implements StateServiceAbstraction -{ - private hasBeenInited = false; - protected isRecoveredSession = false; - - // default account serializer, must be overridden by child class - protected accountDeserializer = Account.fromJSON as (json: Jsonify) => TAccount; - - constructor( - protected storageService: AbstractStorageService, - protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractStorageService, - protected logService: LogService, - protected stateFactory: StateFactory, - protected accountService: AccountService, - protected environmentService: EnvironmentService, - protected tokenService: TokenService, - private migrationRunner: MigrationRunner, - ) {} - - async init(initOptions: InitOptions = {}): Promise { - // Deconstruct and apply defaults - const { runMigrations = true } = initOptions; - if (this.hasBeenInited) { - return; - } - - if (runMigrations) { - await this.migrationRunner.run(); - } else { - // It may have been requested to not run the migrations but we should defensively not - // continue this method until migrations have a chance to be completed elsewhere. - await this.migrationRunner.waitForCompletion(); - } - - await this.state().then(async (state) => { - if (state == null) { - await this.setState(new State(this.createGlobals())); - } else { - this.isRecoveredSession = true; - } - }); - await this.initAccountState(); - - this.hasBeenInited = true; - } - - async initAccountState() { - if (this.isRecoveredSession) { - return; - } - - // Get all likely authenticated accounts - const authenticatedAccounts = await firstValueFrom( - this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))), - ); - - await this.updateState(async (state) => { - for (const i in authenticatedAccounts) { - state = await this.syncAccountFromDisk(authenticatedAccounts[i]); - } - - return state; - }); - } - - async syncAccountFromDisk(userId: string): Promise> { - if (userId == null) { - return; - } - const diskAccount = await this.getAccountFromDisk({ userId: userId }); - const state = await this.updateState(async (state) => { - if (state.accounts == null) { - state.accounts = {}; - } - state.accounts[userId] = this.createAccount(); - - if (diskAccount == null) { - // Return early because we can't set the diskAccount.profile - // if diskAccount itself is null - return state; - } - - state.accounts[userId].profile = diskAccount.profile; - return state; - }); - - return state; - } - - async addAccount(account: TAccount) { - await this.updateState(async (state) => { - state.accounts[account.profile.userId] = account; - return state; - }); - await this.scaffoldNewAccountStorage(account); - } - - async clean(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - await this.deAuthenticateAccount(options.userId); - - await this.removeAccountFromDisk(options?.userId); - await this.removeAccountFromMemory(options?.userId); - } - - /** - * user key when using the "never" option of vault timeout - */ - async getUserKeyAutoUnlock(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}${partialKeys.userAutoKey}`, - options, - ); - } - - /** - * user key when using the "never" option of vault timeout - */ - async setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "auto" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return; - } - await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options); - } - - /** - * User's encrypted symmetric key when using biometrics - */ - async getUserKeyBiometric(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}${partialKeys.userBiometricKey}`, - options, - ); - } - - async hasUserKeyBiometric(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}${partialKeys.userBiometricKey}`, - options, - ); - } - - async setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "biometric" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return; - } - await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options); - } - - async getDuckDuckGoSharedKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get(DDG_SHARED_KEY, options); - } - - async setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - value == null - ? await this.secureStorageService.remove(DDG_SHARED_KEY, options) - : await this.secureStorageService.save(DDG_SHARED_KEY, value, options); - } - - async setEnableDuckDuckGoBrowserIntegration( - value: boolean, - options?: StorageOptions, - ): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableDuckDuckGoBrowserIntegration = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - /** - * @deprecated Use UserKey instead - */ - async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys.cryptoSymmetricKey.encrypted; - } - - async getIsAuthenticated(options?: StorageOptions): Promise { - return ( - (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && - (await this.getUserId(options)) != null - ); - } - - async getUserId(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.userId; - } - - protected async getGlobals(options: StorageOptions): Promise { - let globals: TGlobalState; - if (this.useMemory(options.storageLocation)) { - globals = await this.getGlobalsFromMemory(); - } - - if (this.useDisk && globals == null) { - globals = await this.getGlobalsFromDisk(options); - } - - if (globals == null) { - globals = this.createGlobals(); - } - - return globals; - } - - protected async saveGlobals(globals: TGlobalState, options: StorageOptions) { - return this.useMemory(options.storageLocation) - ? this.saveGlobalsToMemory(globals) - : await this.saveGlobalsToDisk(globals, options); - } - - protected async getGlobalsFromMemory(): Promise { - return (await this.state()).globals; - } - - protected async getGlobalsFromDisk(options: StorageOptions): Promise { - return await this.storageService.get(keys.global, options); - } - - protected async saveGlobalsToMemory(globals: TGlobalState): Promise { - await this.updateState(async (state) => { - state.globals = globals; - return state; - }); - } - - protected async saveGlobalsToDisk(globals: TGlobalState, options: StorageOptions): Promise { - if (options.useSecureStorage) { - await this.secureStorageService.save(keys.global, globals, options); - } else { - await this.storageService.save(keys.global, globals, options); - } - } - - protected async getAccount(options: StorageOptions): Promise { - try { - let account: TAccount; - if (this.useMemory(options.storageLocation)) { - account = await this.getAccountFromMemory(options); - } - - if (this.useDisk(options.storageLocation) && account == null) { - account = await this.getAccountFromDisk(options); - } - - return account; - } catch (e) { - this.logService.error(e); - } - } - - protected async getAccountFromMemory(options: StorageOptions): Promise { - const userId = - options.userId ?? - (await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - )); - - return await this.state().then(async (state) => { - if (state.accounts == null) { - return null; - } - return state.accounts[userId]; - }); - } - - protected async getAccountFromDisk(options: StorageOptions): Promise { - const userId = - options.userId ?? - (await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - )); - - if (userId == null) { - return null; - } - - const account = options?.useSecureStorage - ? ((await this.secureStorageService.get(options.userId, options)) ?? - (await this.storageService.get( - options.userId, - this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }), - ))) - : await this.storageService.get(options.userId, options); - return account; - } - - protected useMemory(storageLocation: StorageLocation) { - return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both; - } - - protected useDisk(storageLocation: StorageLocation) { - return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both; - } - - protected async saveAccount( - account: TAccount, - options: StorageOptions = { - storageLocation: StorageLocation.Both, - useSecureStorage: false, - }, - ) { - return this.useMemory(options.storageLocation) - ? await this.saveAccountToMemory(account) - : await this.saveAccountToDisk(account, options); - } - - protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise { - const storageLocation = options.useSecureStorage - ? this.secureStorageService - : this.storageService; - - await storageLocation.save(`${options.userId}`, account, options); - } - - protected async saveAccountToMemory(account: TAccount): Promise { - if ((await this.getAccountFromMemory({ userId: account.profile.userId })) !== null) { - await this.updateState((state) => { - return new Promise((resolve) => { - state.accounts[account.profile.userId] = account; - resolve(state); - }); - }); - } - } - - protected async scaffoldNewAccountStorage(account: TAccount): Promise { - // We don't want to manipulate the referenced in memory account - const deepClone = JSON.parse(JSON.stringify(account)); - await this.scaffoldNewAccountLocalStorage(deepClone); - await this.scaffoldNewAccountSessionStorage(deepClone); - await this.scaffoldNewAccountMemoryStorage(deepClone); - } - - // TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService. - // For now these methods exist with some redundancy to facilitate this special web requirement. - protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise { - await this.saveAccount( - account, - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskLocalOptions(), - ), - ); - } - - protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise { - await this.storageService.save( - account.profile.userId, - account, - await this.defaultOnDiskMemoryOptions(), - ); - await this.saveAccount( - account, - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskMemoryOptions(), - ), - ); - } - - protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise { - await this.storageService.save( - account.profile.userId, - account, - await this.defaultOnDiskMemoryOptions(), - ); - await this.saveAccount( - account, - this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()), - ); - } - - protected 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; - } - - protected async defaultInMemoryOptions(): Promise { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - return { - storageLocation: StorageLocation.Memory, - userId, - }; - } - - protected async defaultOnDiskOptions(): Promise { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Session, - userId, - useSecureStorage: false, - }; - } - - protected async defaultOnDiskLocalOptions(): Promise { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Local, - userId, - useSecureStorage: false, - }; - } - - protected async defaultOnDiskMemoryOptions(): Promise { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Memory, - userId, - useSecureStorage: false, - }; - } - - protected async defaultSecureStorageOptions(): Promise { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - return { - storageLocation: StorageLocation.Disk, - useSecureStorage: true, - userId, - }; - } - - protected async getActiveUserIdFromStorage(): Promise { - return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); - } - - protected async removeAccountFromLocalStorage(userId: string = null): Promise { - userId ??= await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - const storedAccount = await this.getAccount( - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), - ); - await this.saveAccount( - this.resetAccount(storedAccount), - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), - ); - } - - protected async removeAccountFromSessionStorage(userId: string = null): Promise { - userId ??= await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - const storedAccount = await this.getAccount( - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), - ); - await this.saveAccount( - this.resetAccount(storedAccount), - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), - ); - } - - protected async removeAccountFromSecureStorage(userId: string = null): Promise { - userId ??= await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - await this.setUserKeyAutoUnlock(null, { userId: userId }); - await this.setUserKeyBiometric(null, { userId: userId }); - } - - protected async removeAccountFromMemory(userId: string = null): Promise { - userId ??= await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - - await this.updateState(async (state) => { - delete state.accounts[userId]; - return state; - }); - } - - // settings persist even on reset, and are not affected by this method - protected resetAccount(account: TAccount) { - // All settings have been moved to StateProviders - return this.createAccount(); - } - - protected createAccount(init: Partial = null): TAccount { - return this.stateFactory.createAccount(init); - } - - protected createGlobals(init: Partial = null): TGlobalState { - return this.stateFactory.createGlobal(init); - } - - protected async deAuthenticateAccount(userId: string): Promise { - // We must have a manual call to clear tokens as we can't leverage state provider to clean - // up our data as we have secure storage in the mix. - await this.tokenService.clearTokens(userId as UserId); - } - - protected async removeAccountFromDisk(userId: string) { - await this.removeAccountFromSessionStorage(userId); - await this.removeAccountFromLocalStorage(userId); - await this.removeAccountFromSecureStorage(userId); - } - - protected async saveSecureStorageKey( - key: string, - value: T | null, - options?: StorageOptions, - ) { - return value == null - ? await this.secureStorageService.remove(`${options.userId}${key}`, options) - : await this.secureStorageService.save(`${options.userId}${key}`, value, options); - } - - protected async state(): Promise> { - let state = await this.memoryStorageService.get>(keys.state); - if (this.memoryStorageService.valuesRequireDeserialization) { - state = State.fromJSON(state, this.accountDeserializer); - } - return state; - } - - private async setState( - state: State, - ): Promise> { - await this.memoryStorageService.save(keys.state, state); - return state; - } - - protected async updateState( - stateUpdater: (state: State) => Promise>, - ): Promise> { - return await this.state().then(async (state) => { - const updatedState = await stateUpdater(state); - if (updatedState == null) { - throw new Error("Attempted to update state to null value"); - } - - return await this.setState(updatedState); - }); - } -} diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 40419a343da..45a127c599a 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -9,6 +9,7 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "../../abstractions/api.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; +import { TokenService } from "../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { SyncCipherNotification, @@ -26,7 +27,6 @@ import { SyncService } from "../../vault/abstractions/sync/sync.service.abstract import { CipherData } from "../../vault/models/data/cipher.data"; import { FolderData } from "../../vault/models/data/folder.data"; import { LogService } from "../abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; import { StateProvider, SYNC_DISK, UserKeyDefinition } from "../state"; @@ -44,7 +44,7 @@ export abstract class CoreSyncService implements SyncService { syncInProgress = false; constructor( - protected readonly stateService: StateService, + readonly tokenService: TokenService, protected readonly folderService: InternalFolderService, protected readonly folderApiService: FolderApiServiceAbstraction, protected readonly messageSender: MessageSender, @@ -256,7 +256,13 @@ export abstract class CoreSyncService implements SyncService { async syncDeleteSend(notification: SyncSendNotification): Promise { this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + if ( + activeUserId != null && + (await firstValueFrom(this.tokenService.hasAccessToken$(activeUserId))) + ) { await this.sendService.delete(notification.id); this.messageSender.send("syncedDeletedSend", { sendId: notification.id }); // TODO: Update syncCompleted userId when send service allows modification of non-active users diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index fc6b9481bd5..8929e74c635 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -36,7 +36,6 @@ import { CipherService } from "../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; import { LogService } from "../abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; import { StateProvider } from "../state"; @@ -57,7 +56,6 @@ describe("DefaultSyncService", () => { let sendService: MockProxy; let logService: MockProxy; let keyConnectorService: MockProxy; - let stateService: MockProxy; let providerService: MockProxy; let folderApiService: MockProxy; let organizationService: MockProxy; @@ -86,7 +84,6 @@ describe("DefaultSyncService", () => { sendService = mock(); logService = mock(); keyConnectorService = mock(); - stateService = mock(); providerService = mock(); folderApiService = mock(); organizationService = mock(); @@ -113,7 +110,6 @@ describe("DefaultSyncService", () => { sendService, logService, keyConnectorService, - stateService, providerService, folderApiService, organizationService, diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 99e87383657..9ef7b432d9c 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -53,7 +53,6 @@ import { FolderData } from "../../vault/models/data/folder.data"; import { CipherResponse } from "../../vault/models/response/cipher.response"; import { FolderResponse } from "../../vault/models/response/folder.response"; import { LogService } from "../abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; import { StateProvider } from "../state"; @@ -87,7 +86,6 @@ export class DefaultSyncService extends CoreSyncService { sendService: InternalSendService, logService: LogService, private keyConnectorService: KeyConnectorService, - stateService: StateService, private providerService: ProviderService, folderApiService: FolderApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, @@ -96,12 +94,12 @@ export class DefaultSyncService extends CoreSyncService { private avatarService: AvatarService, private logoutCallback: (logoutReason: LogoutReason, userId?: UserId) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, - private tokenService: TokenService, + tokenService: TokenService, authService: AuthService, stateProvider: StateProvider, ) { super( - stateService, + tokenService, folderService, folderApiService, messageSender, diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index be72d618dee..2088f50d1cc 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -20,7 +20,6 @@ import { EncString } from "../../key-management/crypto/models/enc-string"; import { UriMatchStrategy } from "../../models/domain/domain-service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; -import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; @@ -94,7 +93,6 @@ let accountService: FakeAccountService; describe("Cipher Service", () => { const keyService = mock(); - const stateService = mock(); const autofillSettingsService = mock(); const domainSettingsService = mock(); const apiService = mock(); @@ -127,7 +125,6 @@ describe("Cipher Service", () => { apiService, i18nService, searchService, - stateService, autofillSettingsService, encryptService, cipherFileUploadService, @@ -470,8 +467,6 @@ describe("Cipher Service", () => { searchService.indexedEntityId$.mockReturnValue(of(null)); - stateService.getUserId.mockResolvedValue(mockUserId); - const keys = { userKey: originalUserKey } as CipherDecryptionKeys; keyService.cipherDecryptionKeys$.mockReturnValue(of(keys)); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 96a9a309c54..2f225d4dfc5 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -31,7 +31,6 @@ import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; -import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; @@ -110,7 +109,6 @@ export class CipherService implements CipherServiceAbstraction { private apiService: ApiService, private i18nService: I18nService, private searchService: SearchService, - private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 4942279e436..92bee383a0b 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -732,7 +732,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { protected async getKeyFromStorage( keySuffix: KeySuffixOptions, - userId?: UserId, + userId: UserId, ): Promise { if (keySuffix === KeySuffixOptions.Auto) { const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId }); diff --git a/libs/state/src/index.ts b/libs/state/src/index.ts index d74e7fc137d..71563ecb764 100644 --- a/libs/state/src/index.ts +++ b/libs/state/src/index.ts @@ -2,3 +2,4 @@ export * from "./core"; export * from "./state-migrations"; export * from "./types/state"; +export * from "./legacy"; diff --git a/libs/state/src/legacy/default-state.service.ts b/libs/state/src/legacy/default-state.service.ts new file mode 100644 index 00000000000..b1c5ddb3a0b --- /dev/null +++ b/libs/state/src/legacy/default-state.service.ts @@ -0,0 +1,107 @@ +import { firstValueFrom } from "rxjs"; + +import { StorageService } from "@bitwarden/storage-core"; +import { UserId } from "@bitwarden/user-core"; + +import { ActiveUserAccessor } from "../core"; + +import { GlobalState } from "./global-state"; +import { RequiredUserId, StateService } from "./state.service"; + +const keys = { + global: "global", +}; + +const partialKeys = { + userAutoKey: "_user_auto", + userBiometricKey: "_user_biometric", +}; + +const DDG_SHARED_KEY = "DuckDuckGoSharedKey"; + +export class DefaultStateService implements StateService { + constructor( + private readonly storageService: StorageService, + private readonly secureStorageService: StorageService, + private readonly activeUserAccessor: ActiveUserAccessor, + ) {} + + async clean(options: RequiredUserId): Promise { + await this.setUserKeyAutoUnlock(null, options); + await this.clearUserKeyBiometric(options.userId); + } + + /** + * user key when using the "never" option of vault timeout + */ + async getUserKeyAutoUnlock(options: RequiredUserId): Promise { + if (options.userId == null) { + return null; + } + return await this.secureStorageService.get( + `${options.userId}${partialKeys.userAutoKey}`, + { + userId: options.userId, + keySuffix: "auto", + }, + ); + } + + /** + * user key when using the "never" option of vault timeout + */ + async setUserKeyAutoUnlock(value: string | null, options: RequiredUserId): Promise { + if (options.userId == null) { + return; + } + await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options.userId, "auto"); + } + + private async clearUserKeyBiometric(userId: UserId): Promise { + if (userId == null) { + return; + } + await this.saveSecureStorageKey(partialKeys.userBiometricKey, null, userId, "biometric"); + } + + async getDuckDuckGoSharedKey(): Promise { + const userId = await this.getActiveUserIdFromStorage(); + if (userId == null) { + return null; + } + return await this.secureStorageService.get(DDG_SHARED_KEY); + } + + async setDuckDuckGoSharedKey(value: string): Promise { + const userId = await this.getActiveUserIdFromStorage(); + if (userId == null) { + return; + } + value == null + ? await this.secureStorageService.remove(DDG_SHARED_KEY) + : await this.secureStorageService.save(DDG_SHARED_KEY, value); + } + + async setEnableDuckDuckGoBrowserIntegration(value: boolean): Promise { + const globals = (await this.storageService.get(keys.global)) ?? new GlobalState(); + globals.enableDuckDuckGoBrowserIntegration = value; + await this.storageService.save(keys.global, globals); + } + + private async getActiveUserIdFromStorage(): Promise { + return await firstValueFrom(this.activeUserAccessor.activeUserId$); + } + + private async saveSecureStorageKey( + key: string, + value: string | null, + userId: UserId, + keySuffix: string, + ) { + return value == null + ? await this.secureStorageService.remove(`${userId}${key}`, { keySuffix: keySuffix }) + : await this.secureStorageService.save(`${userId}${key}`, value, { + keySuffix: keySuffix, + }); + } +} diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/state/src/legacy/global-state.ts similarity index 100% rename from libs/common/src/platform/models/domain/global-state.ts rename to libs/state/src/legacy/global-state.ts diff --git a/libs/state/src/legacy/index.ts b/libs/state/src/legacy/index.ts new file mode 100644 index 00000000000..d25d4d4616a --- /dev/null +++ b/libs/state/src/legacy/index.ts @@ -0,0 +1,2 @@ +export { StateService } from "./state.service"; +export { DefaultStateService } from "./default-state.service"; diff --git a/libs/state/src/legacy/state.service.ts b/libs/state/src/legacy/state.service.ts new file mode 100644 index 00000000000..dd07a975895 --- /dev/null +++ b/libs/state/src/legacy/state.service.ts @@ -0,0 +1,25 @@ +import { UserId } from "@bitwarden/user-core"; + +export type RequiredUserId = { userId: UserId }; + +/** + * This class exists for various legacy reasons, there are likely better things to use than this service. + */ +export abstract class StateService { + abstract clean(options: RequiredUserId): Promise; + + /** + * Gets the user's auto key + */ + abstract getUserKeyAutoUnlock(options: RequiredUserId): Promise; + /** + * Sets the user's auto key + */ + abstract setUserKeyAutoUnlock(value: string | null, options: RequiredUserId): Promise; + /** + * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService + */ + abstract setEnableDuckDuckGoBrowserIntegration(value: boolean): Promise; + abstract getDuckDuckGoSharedKey(): Promise; + abstract setDuckDuckGoSharedKey(value: string): Promise; +}