diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index b0b9e3748f8..d7bc45bcc37 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -1,5 +1,3 @@ -import { BehaviorSubject } from "rxjs"; - import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -15,21 +13,13 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService } from "./abstractions/browser-state.service"; -@browserSession export class DefaultBrowserStateService extends BaseStateService implements BrowserStateService { - @sessionSync({ - initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account - initializeAs: "record", - }) - protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>; - protected accountDeserializer = Account.fromJSON; constructor( diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4e540efdc66..bf4bec789ab 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -218,8 +218,10 @@ export class AppComponent implements OnInit, OnDestroy { await this.vaultTimeoutService.lock(message.userId); break; case "lockAllVaults": { - const currentUser = await this.stateService.getUserId(); - const accounts = await firstValueFrom(this.stateService.accounts$); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + const accounts = await firstValueFrom(this.accountService.accounts$); await this.vaultTimeoutService.lock(currentUser); for (const account of Object.keys(accounts)) { if (account === currentUser) { @@ -690,7 +692,7 @@ export class AppComponent implements OnInit, OnDestroy { } private async checkForSystemTimeout(timeout: number): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); for (const userId in accounts) { if (userId == null) { continue; diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a485b925ba6..2acf6dde5a5 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -221,7 +221,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EncryptedMessageHandlerService, deps: [ - StateServiceAbstraction, + AccountServiceAbstraction, AuthServiceAbstraction, CipherServiceAbstraction, PolicyServiceAbstraction, diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index e38339d5ad0..4512e175ceb 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -1,12 +1,13 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -28,7 +29,7 @@ import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-me export class EncryptedMessageHandlerService { constructor( - private stateService: StateService, + private accountService: AccountService, private authService: AuthService, private cipherService: CipherService, private policyService: PolicyService, @@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService { } private async checkUserStatus(userId: string): Promise { - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (userId !== activeUserId) { return "not-active-user"; @@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService { } private async statusCommandHandler(): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); - const activeUserId = await this.stateService.getUserId(); + const accounts = await firstValueFrom(this.accountService.accounts$); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (!accounts || !Object.keys(accounts)) { return []; } return Promise.all( - Object.keys(accounts).map(async (userId) => { + Object.keys(accounts).map(async (userId: UserId) => { const authStatus = await this.authService.getAuthStatus(userId); - const email = await this.stateService.getEmail({ userId }); + const email = accounts[userId].email; return { id: userId, @@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService { } const ciphersResponse: CipherResponse[] = []; - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const authStatus = await this.authService.getAuthStatus(activeUserId); if (authStatus !== AuthenticationStatus.Unlocked) { diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 01d94769777..48bdc600476 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -41,6 +42,7 @@ export class NativeMessagingService { private biometricStateService: BiometricStateService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, + private accountService: AccountService, private ngZone: NgZone, ) {} @@ -51,9 +53,7 @@ export class NativeMessagingService { private async messageHandler(msg: LegacyMessageWrapper | Message) { const outerMessage = msg as Message; if (outerMessage.version) { - // 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.nativeMessageHandler.handleMessage(outerMessage); + await this.nativeMessageHandler.handleMessage(outerMessage); return; } @@ -64,7 +64,7 @@ export class NativeMessagingService { const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey); // Validate the UserId to ensure we are logged into the same account. - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); const userIds = Object.keys(accounts); if (!userIds.includes(rawMessage.userId)) { ipc.platform.nativeMessaging.sendMessage({ @@ -81,7 +81,7 @@ export class NativeMessagingService { }); const fingerprint = await this.cryptoService.getFingerprint( - await this.stateService.getUserId(), + rawMessage.userId, remotePublicKey, ); @@ -98,9 +98,7 @@ export class NativeMessagingService { } } - // 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.secureCommunication(remotePublicKey, appId); + await this.secureCommunication(remotePublicKey, appId); return; } @@ -144,9 +142,7 @@ export class NativeMessagingService { ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) : this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId); if (!(await biometricUnlockPromise)) { - // 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.send({ command: "biometricUnlock", response: "not enabled" }, appId); + await this.send({ command: "biometricUnlock", response: "not enabled" }, appId); return this.ngZone.run(() => this.dialogService.openSimpleDialog({ @@ -172,9 +168,7 @@ export class NativeMessagingService { // we send the master key still for backwards compatibility // with older browser extensions // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - // 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.send( + await this.send( { command: "biometricUnlock", response: "unlocked", @@ -184,14 +178,10 @@ export class NativeMessagingService { appId, ); } else { - // 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.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } } catch (e) { - // 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.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } break; diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 1d54726727b..6de3fd9d8b4 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => { } as EmergencyAccessTakeoverResponse); const mockDecryptedGrantorUserKey = new Uint8Array(64); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey); const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; @@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => { kdf: KdfType.PBKDF2_SHA256, kdfIterations: 500, } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); await expect( emergencyAccessService.takeover(mockId, mockEmail, mockName), @@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => { expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); }); + + it("should throw an error if the users private key cannot be retrieved", async () => { + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({ + keyEncrypted: "EncryptedKey", + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 500, + } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(null); + + await expect(emergencyAccessService.takeover(mockId, mockEmail, mockName)).rejects.toThrow( + "user does not have a private key", + ); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); }); describe("getRotatedKeys", () => { diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index dbc1ce820c6..819b80c1ad7 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -209,7 +209,16 @@ export class EmergencyAccessService { async getViewOnlyCiphers(id: string): Promise { const response = await this.emergencyAccessApiService.postEmergencyAccessView(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot get view only ciphers."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + response.keyEncrypted, + activeUserPrivateKey, + ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; const ciphers = await this.encryptService.decryptItems( @@ -229,7 +238,16 @@ export class EmergencyAccessService { async takeover(id: string, masterPassword: string, email: string) { const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot complete a takeover."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + takeoverResponse.keyEncrypted, + activeUserPrivateKey, + ); if (grantorKeyBuffer == null) { throw new Error("Failed to decrypt grantor key"); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 6875c3816b0..7a96bdc7c70 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,17 +1,17 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, from } from "rxjs"; -import { concatMap, switchMap, takeUntil } from "rxjs/operators"; +import { firstValueFrom, from, map } from "rxjs"; +import { switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -40,10 +40,6 @@ export class ClientsComponent extends BaseClientsComponent { manageOrganizations = false; showAddExisting = false; - protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - constructor( private router: Router, private providerService: ProviderService, @@ -75,15 +71,10 @@ export class ClientsComponent extends BaseClientsComponent { .pipe( switchMap((params) => { this.providerId = params.providerId; - return combineLatest([ - this.providerService.get(this.providerId), - this.consolidatedBillingEnabled$, - ]).pipe( - concatMap(([provider, consolidatedBillingEnabled]) => { - if ( - consolidatedBillingEnabled && - provider.providerStatus === ProviderStatusType.Billable - ) { + return this.providerService.get$(this.providerId).pipe( + canAccessBilling(this.configService), + map((canAccessBilling) => { + if (canAccessBilling) { return from( this.router.navigate(["../manage-client-organizations"], { relativeTo: this.activatedRoute, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 55efbe13864..a1cf2cc5aab 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -1,5 +1,5 @@ -