diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 3f0453f85b..78140ba4af 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -119,7 +119,7 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider { states: Map> = new Map(); constructor(public accountService: FakeAccountService) { - this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a.id)); + this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id)); } get(keyDefinition: KeyDefinition | UserKeyDefinition): ActiveUserState { diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 21ddccb9d6..a5a2d45233 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -4,7 +4,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; -import { OrganizationId, ProviderId } from "../../types/guid"; +import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; import { KeySuffixOptions, KdfType, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; @@ -62,12 +62,15 @@ export abstract class CryptoService { getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise; /** + * Determines whether the user key is available for the given user. + * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ - hasUserKey: () => Promise; + hasUserKey: (userId?: UserId) => Promise; /** - * @param userId The desired user - * @returns True if the user key is set in memory + * Determines whether the user key is available for the given user in memory. + * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. + * @returns True if the user key is available */ hasUserKeyInMemory: (userId?: string) => Promise; /** diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 3f3d3f6636..9160664aa5 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; @@ -108,6 +108,52 @@ describe("cryptoService", () => { }); }); + describe.each(["hasUserKey", "hasUserKeyInMemory"])( + `%s`, + (method: "hasUserKey" | "hasUserKeyInMemory") => { + let mockUserKey: UserKey; + + beforeEach(() => { + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + }); + + it.each([true, false])("returns %s if the user key is set", async (hasKey) => { + stateProvider.singleUser + .getFake(mockUserId, USER_KEY) + .nextState(hasKey ? mockUserKey : null); + expect(await cryptoService[method](mockUserId)).toBe(hasKey); + }); + + it("returns false when no active userId is set", async () => { + accountService.activeAccountSubject.next(null); + expect(await cryptoService[method]()).toBe(false); + }); + + it.each([true, false])( + "resolves %s for active user id when none is provided", + async (hasKey) => { + stateProvider.activeUserId$ = of(mockUserId); + stateProvider.singleUser + .getFake(mockUserId, USER_KEY) + .nextState(hasKey ? mockUserKey : null); + expect(await cryptoService[method]()).toBe(hasKey); + }, + ); + }, + ); + + describe("hasUserKey", () => { + it.each([true, false])( + "returns %s when the user key is not in memory, but the auto key is set", + async (hasKey) => { + stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null); + cryptoService.hasUserKeyStored = jest.fn().mockResolvedValue(hasKey); + expect(await cryptoService.hasUserKey(mockUserId)).toBe(hasKey); + }, + ); + }); + describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 9bfa6d661b..86f7c3798f 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -202,13 +202,23 @@ export class CryptoService implements CryptoServiceAbstraction { } } - async hasUserKey(): Promise { + async hasUserKey(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (userId == null) { + return false; + } return ( - (await this.hasUserKeyInMemory()) || (await this.hasUserKeyStored(KeySuffixOptions.Auto)) + (await this.hasUserKeyInMemory(userId)) || + (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) ); } async hasUserKeyInMemory(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (userId == null) { + return false; + } + return (await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId))) != null; }