diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index e5a4087510..e31b40fe81 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -75,6 +75,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { ProcessReloadService } from "@bitwarden/common/key-management/services/process-reload.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -270,6 +272,7 @@ import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; + export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; @@ -314,6 +317,7 @@ export default class MainBackground { badgeSettingsService: BadgeSettingsServiceAbstraction; domainSettingsService: DomainSettingsService; systemService: SystemServiceAbstraction; + processReloadService: ProcessReloadServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction; eventUploadService: EventUploadServiceAbstraction; policyService: InternalPolicyServiceAbstraction; @@ -408,7 +412,7 @@ export default class MainBackground { await this.refreshMenu(true); if (this.systemService != null) { await this.systemService.clearPendingClipboard(); - await this.systemService.startProcessReload(this.authService); + await this.processReloadService.startProcessReload(this.authService); } }; @@ -1088,15 +1092,18 @@ export default class MainBackground { }; this.systemService = new SystemService( + this.platformUtilsService, + this.autofillSettingsService, + this.taskSchedulerService, + ); + + this.processReloadService = new ProcessReloadService( this.pinService, this.messagingService, - this.platformUtilsService, systemUtilsServiceReloadCallback, - this.autofillSettingsService, this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, - this.taskSchedulerService, ); // Other fields @@ -1122,7 +1129,7 @@ export default class MainBackground { this.platformUtilsService as BrowserPlatformUtilsService, this.notificationsService, this.autofillSettingsService, - this.systemService, + this.processReloadService, this.environmentService, this.messagingService, this.logService, @@ -1551,7 +1558,7 @@ export default class MainBackground { await this.mainContextMenuHandler?.noAccess(); await this.notificationsService.updateConnection(false); await this.systemService.clearPendingClipboard(); - await this.systemService.startProcessReload(this.authService); + await this.processReloadService.startProcessReload(this.authService); } private async needsStorageReseed(userId: UserId): Promise { diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 2bc2eadf26..f934c8544b 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -6,10 +6,10 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -40,7 +40,7 @@ export default class RuntimeBackground { private platformUtilsService: BrowserPlatformUtilsService, private notificationsService: NotificationsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, - private systemService: SystemService, + private processReloadSerivce: ProcessReloadServiceAbstraction, private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, @@ -216,7 +216,7 @@ export default class RuntimeBackground { } await this.notificationsService.updateConnection(msg.command === "loggedIn"); - this.systemService.cancelProcessReload(); + this.processReloadSerivce.cancelProcessReload(); if (item) { await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index dceda128c8..83dc1619fa 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -32,6 +32,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -142,6 +143,7 @@ export class AppComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private platformUtilsService: PlatformUtilsService, private systemService: SystemService, + private processReloadService: ProcessReloadServiceAbstraction, private stateService: StateService, private eventUploadService: EventUploadService, private policyService: InternalPolicyService, @@ -213,7 +215,7 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateAppMenu(); - this.systemService.cancelProcessReload(); + this.processReloadService.cancelProcessReload(); break; case "loggedOut": this.modalService.closeAll(); @@ -224,7 +226,7 @@ export class AppComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateAppMenu(); await this.systemService.clearPendingClipboard(); - await this.systemService.startProcessReload(this.authService); + await this.processReloadService.startProcessReload(this.authService); break; case "authBlocked": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -268,15 +270,15 @@ export class AppComponent implements OnInit, OnDestroy { this.notificationsService.updateConnection(); await this.updateAppMenu(); await this.systemService.clearPendingClipboard(); - await this.systemService.startProcessReload(this.authService); + await this.processReloadService.startProcessReload(this.authService); break; case "startProcessReload": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.systemService.startProcessReload(this.authService); + this.processReloadService.startProcessReload(this.authService); break; case "cancelProcessReload": - this.systemService.cancelProcessReload(); + this.processReloadService.cancelProcessReload(); break; case "reloadProcess": ipc.platform.reloadProcess(); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index c9b434aa96..3611368442 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -37,6 +37,8 @@ import { import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ClientType } from "@bitwarden/common/enums"; +import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { ProcessReloadService } from "@bitwarden/common/key-management/services/process-reload.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService, @@ -196,16 +198,22 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SystemServiceAbstraction, useClass: SystemService, + deps: [ + PlatformUtilsServiceAbstraction, + AutofillSettingsServiceAbstraction, + TaskSchedulerService, + ], + }), + safeProvider({ + provide: ProcessReloadServiceAbstraction, + useClass: ProcessReloadService, deps: [ PinServiceAbstraction, MessagingServiceAbstraction, - PlatformUtilsServiceAbstraction, RELOAD_CALLBACK, - AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, BiometricStateService, AccountServiceAbstraction, - TaskSchedulerService, ], }), safeProvider({ diff --git a/libs/common/src/key-management/abstractions/process-reload.service.ts b/libs/common/src/key-management/abstractions/process-reload.service.ts new file mode 100644 index 0000000000..e46c1e2319 --- /dev/null +++ b/libs/common/src/key-management/abstractions/process-reload.service.ts @@ -0,0 +1,6 @@ +import { AuthService } from "../../auth/abstractions/auth.service"; + +export abstract class ProcessReloadServiceAbstraction { + abstract startProcessReload(authService: AuthService): Promise; + abstract cancelProcessReload(): void; +} diff --git a/libs/common/src/key-management/services/process-reload.service.ts b/libs/common/src/key-management/services/process-reload.service.ts new file mode 100644 index 0000000000..2f25d63b0f --- /dev/null +++ b/libs/common/src/key-management/services/process-reload.service.ts @@ -0,0 +1,106 @@ +import { firstValueFrom, map, timeout } from "rxjs"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { BiometricStateService } from "@bitwarden/key-management"; + +import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; +import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service"; + +export class ProcessReloadService implements ProcessReloadServiceAbstraction { + private reloadInterval: any = null; + + constructor( + private pinService: PinServiceAbstraction, + private messagingService: MessagingService, + private reloadCallback: () => Promise = null, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private biometricStateService: BiometricStateService, + private accountService: AccountService, + ) {} + + async startProcessReload(authService: AuthService): Promise { + const accounts = await firstValueFrom(this.accountService.accounts$); + if (accounts != null) { + const keys = Object.keys(accounts); + if (keys.length > 0) { + for (const userId of keys) { + let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); + status = await authService.getAuthStatus(userId); + if (status === AuthenticationStatus.Unlocked) { + return; + } + } + } + } + + // A reloadInterval has already been set and is executing + if (this.reloadInterval != null) { + return; + } + + // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock. + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (userId != null) { + const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); + if (ephemeralPin != null) { + return; + } + } + + this.cancelProcessReload(); + await this.executeProcessReload(); + } + + private async executeProcessReload() { + const biometricLockedFingerprintValidated = await firstValueFrom( + this.biometricStateService.fingerprintValidated$, + ); + if (!biometricLockedFingerprintValidated) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout(500), + ), + ); + // Replace current active user if they will be logged out on reload + if (activeUserId != null) { + const timeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService + .getVaultTimeoutActionByUserId$(activeUserId) + .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory + ); + if (timeoutAction === VaultTimeoutAction.LogOut) { + const nextUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), + ); + await this.accountService.switchAccount(nextUser); + } + } + + this.messagingService.send("reloadProcess"); + if (this.reloadCallback != null) { + await this.reloadCallback(); + } + return; + } + if (this.reloadInterval == null) { + this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); + } + } + + cancelProcessReload(): void { + if (this.reloadInterval != null) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + } + } +} diff --git a/libs/common/src/platform/abstractions/system.service.ts b/libs/common/src/platform/abstractions/system.service.ts index 204e336fbf..7a34a31352 100644 --- a/libs/common/src/platform/abstractions/system.service.ts +++ b/libs/common/src/platform/abstractions/system.service.ts @@ -1,8 +1,4 @@ -import { AuthService } from "../../auth/abstractions/auth.service"; - export abstract class SystemService { - abstract startProcessReload(authService: AuthService): Promise; - abstract cancelProcessReload(): void; abstract clearClipboard(clipboardValue: string, timeoutMs?: number): Promise; abstract clearPendingClipboard(): Promise; } diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 357737391c..03e96af75b 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,16 +1,6 @@ -import { firstValueFrom, map, Subscription, timeout } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; -import { BiometricStateService } from "@bitwarden/key-management"; - -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; -import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; -import { AccountService } from "../../auth/abstractions/account.service"; -import { AuthService } from "../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; -import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { UserId } from "../../types/guid"; -import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { Utils } from "../misc/utils"; @@ -18,19 +8,12 @@ import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; export class SystemService implements SystemServiceAbstraction { - private reloadInterval: any = null; private clearClipboardTimeoutSubscription: Subscription; private clearClipboardTimeoutFunction: () => Promise = null; constructor( - private pinService: PinServiceAbstraction, - private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, - private reloadCallback: () => Promise = null, private autofillSettingsService: AutofillSettingsServiceAbstraction, - private vaultTimeoutSettingsService: VaultTimeoutSettingsService, - private biometricStateService: BiometricStateService, - private accountService: AccountService, private taskSchedulerService: TaskSchedulerService, ) { this.taskSchedulerService.registerTaskHandler( @@ -39,86 +22,6 @@ export class SystemService implements SystemServiceAbstraction { ); } - async startProcessReload(authService: AuthService): Promise { - const accounts = await firstValueFrom(this.accountService.accounts$); - if (accounts != null) { - const keys = Object.keys(accounts); - if (keys.length > 0) { - for (const userId of keys) { - let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); - status = await authService.getAuthStatus(userId); - if (status === AuthenticationStatus.Unlocked) { - return; - } - } - } - } - - // A reloadInterval has already been set and is executing - if (this.reloadInterval != null) { - return; - } - - // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (userId != null) { - const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); - if (ephemeralPin != null) { - return; - } - } - - this.cancelProcessReload(); - await this.executeProcessReload(); - } - - private async executeProcessReload() { - const biometricLockedFingerprintValidated = await firstValueFrom( - this.biometricStateService.fingerprintValidated$, - ); - if (!biometricLockedFingerprintValidated) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe( - map((a) => a?.id), - timeout(500), - ), - ); - // Replace current active user if they will be logged out on reload - if (activeUserId != null) { - const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService - .getVaultTimeoutActionByUserId$(activeUserId) - .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory - ); - if (timeoutAction === VaultTimeoutAction.LogOut) { - const nextUser = await firstValueFrom( - this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), - ); - await this.accountService.switchAccount(nextUser); - } - } - - this.messagingService.send("reloadProcess"); - if (this.reloadCallback != null) { - await this.reloadCallback(); - } - return; - } - if (this.reloadInterval == null) { - this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); - } - } - - cancelProcessReload(): void { - if (this.reloadInterval != null) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - } - } - async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { this.clearClipboardTimeoutSubscription?.unsubscribe();