diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index a4ebba7a760..7c081b38279 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -31,6 +31,7 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -41,7 +42,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; @@ -654,7 +655,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { orgId = this.selfHosted ? await this.createSelfHosted(key, collectionCt, orgKeys) - : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); + : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); this.toastService.showToast({ variant: "success", @@ -808,6 +809,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { collectionCt: string, orgKeys: [string, EncString], orgKey: SymmetricCryptoKey, + activeUserId: UserId, ): Promise { const request = new OrganizationCreateRequest(); request.key = key; @@ -855,7 +857,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formGroup.controls.clientOwnerEmail.value, request, ); - const providerKey = await this.keyService.getProviderKey(this.providerId); + + const providerKey = await firstValueFrom( + this.keyService + .providerKeys$(activeUserId) + .pipe(map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null)), + ); + assertNonNullish(providerKey, "Provider key not found"); + providerRequest.organizationCreateRequest.key = ( await this.encryptService.wrapSymmetricKey(orgKey, providerKey) ).encryptedString; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts index e36e4e5f0c6..8ce8153b36e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts @@ -1,8 +1,11 @@ import { Component, Inject, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -46,6 +49,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit { private providerApiService: ProviderApiServiceAbstraction, private toastService: ToastService, private webProviderService: WebProviderService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -57,9 +61,11 @@ export class AddExistingOrganizationDialogComponent implements OnInit { addExistingOrganization = async (): Promise => { if (this.selectedOrganization) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await this.webProviderService.addOrganizationToProvider( this.dialogParams.provider.id, this.selectedOrganization.id, + userId, ); this.toastService.showToast({ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index dd54b842062..7ade77ed01b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject } from "@angular/core"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { OrganizationUserBulkPublicKeyResponse, @@ -12,10 +13,14 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ProviderId } from "@bitwarden/common/types/guid"; +import { ProviderKey } from "@bitwarden/common/types/key"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; @@ -35,6 +40,7 @@ type BulkConfirmDialogParams = { }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { providerId: string; + providerKey$: Observable; constructor( private apiService: ApiService, @@ -42,15 +48,21 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { protected encryptService: EncryptService, @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, protected i18nService: I18nService, + private accountService: AccountService, ) { super(keyService, encryptService, i18nService); this.providerId = dialogParams.providerId; + this.providerKey$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.providerKeys$(userId)), + map((providerKeysById) => providerKeysById?.[this.providerId as ProviderId]), + ); this.users = dialogParams.users; } - protected getCryptoKey = (): Promise => - this.keyService.getProviderKey(this.providerId); + protected getCryptoKey = async (): Promise => + await firstValueFrom(this.providerKey$); protected getPublicKeys = async (): Promise< ListResponse diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index b1cd52cf8a6..268a82ac12f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -4,7 +4,7 @@ import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs"; -import { first } from "rxjs/operators"; +import { first, map } from "rxjs/operators"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -16,11 +16,13 @@ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/mode import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { ProviderId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; @@ -204,7 +206,15 @@ export class MembersComponent extends BaseMembersComponent { async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { try { - const providerKey = await this.keyService.getProviderKey(this.providerId); + const providerKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.providerKeys$(userId)), + map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null), + ), + ); + assertNonNullish(providerKey, "Provider key not found"); + const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey); const request = new ProviderUserConfirmRequest(); request.key = key.encryptedString; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts index b2da18dd047..2accd760fcb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts @@ -1,4 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; @@ -8,7 +9,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { StateProvider } from "@bitwarden/common/platform/state"; import { OrgKey, ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { newGuid } from "@bitwarden/guid"; @@ -24,16 +24,22 @@ describe("WebProviderService", () => { let apiService: MockProxy; let i18nService: MockProxy; let encryptService: MockProxy; - let stateProvider: MockProxy; let providerApiService: MockProxy; + const activeUserId = newGuid() as UserId; + const providerId = "provider-123"; + const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey; + const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey; + const mockProviderKeysById: Record = { + [providerId]: mockProviderKey, + }; + beforeEach(() => { keyService = mock(); syncService = mock(); apiService = mock(); i18nService = mock(); encryptService = mock(); - stateProvider = mock(); providerApiService = mock(); sut = new WebProviderService( @@ -42,14 +48,69 @@ describe("WebProviderService", () => { apiService, i18nService, encryptService, - stateProvider, providerApiService, ); }); + describe("addOrganizationToProvider", () => { + const organizationId = "org-789"; + const encryptedOrgKey = new EncString("encrypted-org-key"); + const mockOrgKeysById: Record = { + [organizationId]: mockOrgKey, + }; + + beforeEach(() => { + keyService.orgKeys$.mockReturnValue(of(mockOrgKeysById)); + keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById)); + encryptService.wrapSymmetricKey.mockResolvedValue(encryptedOrgKey); + }); + + it("adds an organization to a provider with correct encryption", async () => { + await sut.addOrganizationToProvider(providerId, organizationId, activeUserId); + + expect(keyService.orgKeys$).toHaveBeenCalledWith(activeUserId); + expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId); + expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); + expect(providerApiService.addOrganizationToProvider).toHaveBeenCalledWith(providerId, { + key: encryptedOrgKey.encryptedString, + organizationId, + }); + expect(syncService.fullSync).toHaveBeenCalledWith(true); + }); + + it("throws an error if organization key is not found", async () => { + const invalidOrgId = "invalid-org"; + + await expect( + sut.addOrganizationToProvider(providerId, invalidOrgId, activeUserId), + ).rejects.toThrow("Organization key not found"); + }); + + it("throws an error if no organization keys are available", async () => { + keyService.orgKeys$.mockReturnValue(of(null)); + + await expect( + sut.addOrganizationToProvider(providerId, organizationId, activeUserId), + ).rejects.toThrow("Organization key not found"); + }); + + it("throws an error if provider key is not found", async () => { + const invalidProviderId = "invalid-provider"; + await expect( + sut.addOrganizationToProvider(invalidProviderId, organizationId, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); + + it("throws an error if no provider keys are available", async () => { + keyService.providerKeys$.mockReturnValue(of(null)); + + await expect( + sut.addOrganizationToProvider(providerId, organizationId, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); + }); + describe("createClientOrganization", () => { - const activeUserId = newGuid() as UserId; - const providerId = "provider-123"; const name = "Test Org"; const ownerEmail = "owner@example.com"; const planType = PlanType.EnterpriseAnnually; @@ -59,15 +120,13 @@ describe("WebProviderService", () => { const encryptedProviderKey = new EncString("encrypted-provider-key"); const encryptedCollectionName = new EncString("encrypted-collection-name"); const defaultCollectionTranslation = "Default Collection"; - const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey; - const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey; beforeEach(() => { keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]); keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]); i18nService.t.mockReturnValue(defaultCollectionTranslation); encryptService.encryptString.mockResolvedValue(encryptedCollectionName); - keyService.getProviderKey.mockResolvedValue(mockProviderKey); + keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById)); encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey); }); @@ -88,7 +147,7 @@ describe("WebProviderService", () => { defaultCollectionTranslation, mockOrgKey, ); - expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId); + expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId); expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith( @@ -107,5 +166,27 @@ describe("WebProviderService", () => { expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(syncService.fullSync).toHaveBeenCalledWith(true); }); + + it("throws an error if provider key is not found", async () => { + const invalidProviderId = "invalid-provider"; + await expect( + sut.createClientOrganization( + invalidProviderId, + name, + ownerEmail, + planType, + seats, + activeUserId, + ), + ).rejects.toThrow("Provider key not found"); + }); + + it("throws an error if no provider keys are available", async () => { + keyService.providerKeys$.mockReturnValue(of(null)); + + await expect( + sut.createClientOrganization(providerId, name, ownerEmail, planType, seats, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 78931f9c445..e1eea78d26a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -1,18 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { combineLatest, firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { CreateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/create-provider-organization.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { PlanType } from "@bitwarden/common/billing/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; @@ -25,18 +24,26 @@ export class WebProviderService { private apiService: ApiService, private i18nService: I18nService, private encryptService: EncryptService, - private stateProvider: StateProvider, private providerApiService: ProviderApiServiceAbstraction, ) {} - async addOrganizationToProvider(providerId: string, organizationId: string): Promise { - const orgKey = await firstValueFrom( - this.stateProvider.activeUserId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), - ), + async addOrganizationToProvider( + providerId: string, + organizationId: string, + activeUserId: UserId, + ): Promise { + const [orgKeysById, providerKeys] = await firstValueFrom( + combineLatest([ + this.keyService.orgKeys$(activeUserId), + this.keyService.providerKeys$(activeUserId), + ]), ); - const providerKey = await this.keyService.getProviderKey(providerId); + + const orgKey = orgKeysById?.[organizationId as OrganizationId]; + const providerKey = providerKeys?.[providerId as ProviderId]; + assertNonNullish(orgKey, "Organization key not found"); + assertNonNullish(providerKey, "Provider key not found"); + const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); await this.providerApiService.addOrganizationToProvider(providerId, { key: encryptedOrgKey.encryptedString, @@ -62,7 +69,12 @@ export class WebProviderService { organizationKey, ); - const providerKey = await this.keyService.getProviderKey(providerId); + const providerKey = await firstValueFrom( + this.keyService + .providerKeys$(activeUserId) + .pipe(map((providerKeys) => providerKeys?.[providerId as ProviderId])), + ); + assertNonNullish(providerKey, "Provider key not found"); const encryptedProviderKey = await this.encryptService.wrapSymmetricKey( organizationKey, diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index abd4dcc1563..7891c9952b2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -10,7 +10,7 @@ import { import { WrappedSigningKey } from "@bitwarden/common/key-management/types"; import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, @@ -248,17 +248,19 @@ export abstract class KeyService { /** * Stores the provider keys for a given user. - * @param orgs The provider orgs for which to save the keys from. + * @param providers The provider orgs for which to save the keys from. * @param userId The user id of the user for which to store the keys for. */ - abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise; + abstract setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise; + /** - * - * @throws Error when providerId is null or no active user - * @param providerId The desired provider - * @returns The provider's symmetric key + * Gets an observable of provider keys for the given user. + * @param userId The user to get provider keys for. + * @return An observable stream of the users providers keys if they are unlocked, or null if the user is not unlocked. + * @throws If an invalid user id is passed in. */ - abstract getProviderKey(providerId: string): Promise; + abstract providerKeys$(userId: UserId): Observable | null>; + /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 0dd9f3603f5..5d5340d4900 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -39,7 +39,7 @@ import { FakeSingleUserState, } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, @@ -1314,6 +1314,49 @@ describe("keyService", () => { }); }); + describe("providerKeys$", () => { + let mockUserPrivateKey: Uint8Array; + let mockProviderKeys: Record; + + beforeEach(() => { + mockUserPrivateKey = makeStaticByteArray(64, 1); + mockProviderKeys = { + ["provider1" as ProviderId]: makeSymmetricCryptoKey(64), + ["provider2" as ProviderId]: makeSymmetricCryptoKey(64), + }; + }); + + it("returns null when userPrivateKey is null", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(null)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toBeNull(); + }); + + it("returns provider keys when userPrivateKey is available", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any)); + jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(mockProviderKeys)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toEqual(mockProviderKeys); + expect((keyService as any).providerKeysHelper$).toHaveBeenCalledWith( + mockUserId, + mockUserPrivateKey, + ); + }); + + it("returns null when providerKeysHelper$ returns null", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any)); + jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(null)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toBeNull(); + }); + }); + describe("makeKeyPair", () => { test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])( "throws when the provided key is %s", diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index fc340410124..032faeaf42e 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -426,20 +426,16 @@ export class DefaultKeyService implements KeyServiceAbstraction { }); } - // TODO: Deprecate in favor of observable - async getProviderKey(providerId: ProviderId): Promise { - if (providerId == null) { - return null; - } + providerKeys$(userId: UserId): Observable | null> { + return this.userPrivateKey$(userId).pipe( + switchMap((userPrivateKey) => { + if (userPrivateKey == null) { + return of(null); + } - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == null) { - throw new Error("No active user found."); - } - - const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId)); - - return providerKeys?.[providerId] ?? null; + return this.providerKeysHelper$(userId, userPrivateKey); + }), + ); } private async clearProviderKeys(userId: UserId): Promise { @@ -829,18 +825,6 @@ export class DefaultKeyService implements KeyServiceAbstraction { )) as UserPrivateKey; } - providerKeys$(userId: UserId) { - return this.userPrivateKey$(userId).pipe( - switchMap((userPrivateKey) => { - if (userPrivateKey == null) { - return of(null); - } - - return this.providerKeysHelper$(userId, userPrivateKey); - }), - ); - } - /** * A helper for decrypting provider keys that requires a user id and that users decrypted private key * this is helpful for when you may have already grabbed the user private key and don't want to redo