diff --git a/apps/desktop/src/auth/services/desktop-pin.service.spec.ts b/apps/desktop/src/auth/services/desktop-pin.service.spec.ts new file mode 100644 index 00000000000..9eb72a8e2bf --- /dev/null +++ b/apps/desktop/src/auth/services/desktop-pin.service.spec.ts @@ -0,0 +1,137 @@ +import { mock } from "jest-mock-extended"; + +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + FakeAccountService, + FakeStateProvider, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KdfConfigService } from "@bitwarden/key-management"; + +import { DesktopPinService } from "./desktop-pin.service"; + +describe("DesktopPinService", () => { + let sut: DesktopPinService; + + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let stateProvider: FakeStateProvider; + + const cryptoFunctionService = mock(); + const encryptService = mock(); + const kdfConfigService = mock(); + const keyGenerationService = mock(); + const logService = mock(); + const stateService = mock(); + + const mockUserId = Utils.newGuid() as UserId; + const mockUserEmail = "user@example.com"; + + (global as any).ipc = { + platform: { + ephemeralStore: { + getEphemeralValue: jest.fn(), + setEphemeralValue: jest.fn(), + removeEphemeralValue: jest.fn(), + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail }); + masterPasswordService = new FakeMasterPasswordService(); + stateProvider = new FakeStateProvider(accountService); + + sut = new DesktopPinService( + accountService, + cryptoFunctionService, + encryptService, + kdfConfigService, + keyGenerationService, + logService, + masterPasswordService, + stateProvider, + stateService, + ); + }); + + it("should instantiate the PinService", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("userId validation", () => { + it("should throw an error if a userId is not provided", async () => { + await expect(sut.getPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow( + "Cannot get pin key encrypted user key ephemeral without a user ID.", + ); + await expect(sut.clearPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow( + "Cannot delete pin key encrypted user key ephemeral without a user ID.", + ); + await expect( + sut.setPinKeyEncryptedUserKeyEphemeral(new EncString("value"), undefined), + ).rejects.toThrow("Cannot set pin key encrypted user key ephemeral without a user ID."); + }); + }); + + describe("getPinKeyEncryptedUserKeyEphemeral", () => { + it("should return null if the ephemeral value is not found", async () => { + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(null); + + const result = await sut.getPinKeyEncryptedUserKeyEphemeral(mockUserId); + + expect(result).toBeNull(); + }); + + it("should return the EncString if the ephemeral value is found", async () => { + const mockValue = "mock-value"; + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(mockValue); + + const result = await sut.getPinKeyEncryptedUserKeyEphemeral(mockUserId); + + expect(result).toEqual(new EncString(mockValue)); + }); + + it("should call the ephemeral store with the correct key", async () => { + (global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(null); + + await sut.getPinKeyEncryptedUserKeyEphemeral(mockUserId); + + expect((global as any).ipc.platform.ephemeralStore.getEphemeralValue).toHaveBeenCalledWith( + `pinKeyEncryptedUserKeyEphemeral-${mockUserId}`, + ); + }); + }); + + describe("setPinKeyEncryptedUserKeyEphemeral", () => { + it("should call the ephemeral store with the correct key and value", async () => { + const mockValue = new EncString("mock-value"); + + await sut.setPinKeyEncryptedUserKeyEphemeral(mockValue, mockUserId); + + expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith( + `pinKeyEncryptedUserKeyEphemeral-${mockUserId}`, + mockValue.encryptedString, + ); + }); + }); + + describe("clearPinKeyEncryptedUserKeyEphemeral", () => { + it("should call the ephemeral store with the correct key", async () => { + await sut.clearPinKeyEncryptedUserKeyEphemeral(mockUserId); + + expect((global as any).ipc.platform.ephemeralStore.removeEphemeralValue).toHaveBeenCalledWith( + `pinKeyEncryptedUserKeyEphemeral-${mockUserId}`, + ); + }); + }); +}); diff --git a/apps/desktop/src/auth/services/desktop-pin.service.ts b/apps/desktop/src/auth/services/desktop-pin.service.ts index 8c1c727f4d9..a05619aa119 100644 --- a/apps/desktop/src/auth/services/desktop-pin.service.ts +++ b/apps/desktop/src/auth/services/desktop-pin.service.ts @@ -3,7 +3,12 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; export class DesktopPinService extends PinService { - async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise { + override async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise { + super.validateUserId( + userId, + "Cannot get pin key encrypted user key ephemeral without a user ID.", + ); + const ephemeralValue = await ipc.platform.ephemeralStore.getEphemeralValue( `pinKeyEncryptedUserKeyEphemeral-${userId}`, ); @@ -14,14 +19,27 @@ export class DesktopPinService extends PinService { } } - async setPinKeyEncryptedUserKeyEphemeral(value: EncString, userId: UserId): Promise { + override async setPinKeyEncryptedUserKeyEphemeral( + value: EncString, + userId: UserId, + ): Promise { + super.validateUserId( + userId, + "Cannot set pin key encrypted user key ephemeral without a user ID.", + ); + return await ipc.platform.ephemeralStore.setEphemeralValue( `pinKeyEncryptedUserKeyEphemeral-${userId}`, value.encryptedString, ); } - async deletePinKeyEncryptedUserKeyEphemeral(userId: string): Promise { + override async clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise { + super.validateUserId( + userId, + "Cannot delete pin key encrypted user key ephemeral without a user ID.", + ); + return await ipc.platform.ephemeralStore.removeEphemeralValue( `pinKeyEncryptedUserKeyEphemeral-${userId}`, ); diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 204054ef8f0..b3a4626d2ef 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -519,7 +519,7 @@ export class PinService implements PinServiceAbstraction { /** * Throws a custom error message if user ID is not provided. */ - private validateUserId(userId: UserId, errorMessage: string = "") { + protected validateUserId(userId: UserId, errorMessage: string = "") { if (!userId) { throw new Error(`User ID is required. ${errorMessage}`); }