From d3ebadc7fe4c64907b6a71551207d8865fadf3cb Mon Sep 17 00:00:00 2001 From: Jared Snider Date: Sun, 23 Mar 2025 13:24:19 -0400 Subject: [PATCH] DefaultOpaqueKeyExchangeService - add tests for registration --- ...efault-opaque-key-exchange.service.spec.ts | 158 +++++++++++++++++- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.spec.ts b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.spec.ts index 96df2ef5c85..0e213c1e2b4 100644 --- a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.spec.ts +++ b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.spec.ts @@ -1,16 +1,38 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { RotateableKeySet } from "@bitwarden/auth/common"; +import { + BitwardenClient, + ClientRegistrationFinishResult, + ClientRegistrationStartResult, + CryptoClient, + RotateableKeyset, +} from "@bitwarden/sdk-internal"; + +// Must mock modules before importing +jest.mock("../../platform/misc/utils", () => { + const originalModule = jest.requireActual("../../platform/misc/utils"); + + return { + ...originalModule, // avoid losing the original module's exports + fromBufferToB64: jest.fn(), + fromB64ToArray: jest.fn(), + }; +}); import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { LogService } from "../../platform/abstractions/log.service"; import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { Utils } from "../../platform/misc/utils"; +import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { OpaqueSessionId } from "../../types/guid"; import { UserKey } from "../../types/key"; import { DefaultOpaqueKeyExchangeService } from "./default-opaque-key-exchange.service"; import { OpaqueCipherConfiguration } from "./models/opaque-cipher-configuration"; +import { RegistrationStartResponse } from "./models/registration-start.response"; import { OpaqueKeyExchangeApiService } from "./opaque-key-exchange-api.service"; import { OpaqueKeyExchangeService } from "./opaque-key-exchange.service"; @@ -23,6 +45,7 @@ describe("DefaultOpaqueKeyExchangeService", () => { let service: OpaqueKeyExchangeService; let sdkBitwardenClient: BitwardenClient; + let sdkCryptoClient: CryptoClient; beforeEach(() => { opaqueKeyExchangeApiService = mock(); @@ -30,6 +53,8 @@ describe("DefaultOpaqueKeyExchangeService", () => { sdkService = mock(); sdkBitwardenClient = mock(); sdkService.client$ = new BehaviorSubject(sdkBitwardenClient); + sdkCryptoClient = mock(); + sdkBitwardenClient.crypto = jest.fn().mockReturnValue(sdkCryptoClient); encryptService = mock(); logService = mock(); @@ -50,8 +75,14 @@ describe("DefaultOpaqueKeyExchangeService", () => { let masterPassword: string; let userKey: UserKey; let opaqueCipherConfig: OpaqueCipherConfiguration; - // let clientRegistrationStartResult: ClientRegistrationStartResult; - // let registrationStartResponse: RegistrationStartResponse; + let clientRegistrationStartResult: ClientRegistrationStartResult; + let registrationRequestB64: string; + let registrationStartResponse: RegistrationStartResponse; + let registrationResponseArray: Uint8Array; + let registrationUploadB64: string; + let clientRegistrationFinishResult: ClientRegistrationFinishResult; + let sdkRotateableKeyset: RotateableKeyset; + let rotatableKeySet: RotateableKeySet; beforeEach(() => { masterPassword = "masterPassword"; @@ -62,10 +93,29 @@ describe("DefaultOpaqueKeyExchangeService", () => { parallelism: 1, }); - // clientRegistrationStartResult = mock(); - // sdkBitwardenClient.crypto().opaque_register_start.mockReturnValue(clientRegistrationStartResult); + clientRegistrationStartResult = mock(); - // registrationStartResponse = mock(); + registrationStartResponse = mock(); + registrationRequestB64 = "registration_request"; + registrationUploadB64 = "registration_upload"; + + registrationResponseArray = new Uint8Array([1, 2, 3, 4, 5]); + clientRegistrationFinishResult = mock(); + + sdkRotateableKeyset = { + private_key: + "4.A0NkSBITVucCAttGzh600t3wP5bS+MbqMbIsdImumsDR9CQo5E7MJ85IES+S70Miymn1Yvfjva/7XifXdZFfwD/ZnUzDccrc62Dfx0liDyCvLoPyX98UkM/rLjG6+j1qatlmNF4f/ahb+zOOaB8dL9k+fCJkvEEDxgEGU52a5KvE61HtEVy/MjnmCJaKLp4v4Ac0WcDkfs+TbWa5Y+x3i4rxe1K9LmWmPB7AeoFaWnCvwcSuSGhAF4XyQpkvxLlpmmoIfwBgq4FmBszDS+IO7pFXjAfl+JXlzhpcieBWIoHc6Peb7pvfgoYQz8+Ocq/ztS9L/+QdAFyoXh5p/yKAQA==", + public_key: + "2.hrHGpvLlJuzl5U3+/daF/w==|wiklRhnim8A3vSu5eUZZIZM9J8cG/g3OIHGKGaXE9FRPyHFwnMbOMWjLsHV5m/zx647HTpaesblwG36Rwy2cf7bu6BPny2COz4f+50MWUBqLNM2o09pyDZsaKqlwfnRAvcBst3f5YuIx/4DFGSfwZwutdzGkQMc9/oxXw0mMqiDeGk7fsSBzJSo781kuLQvnD1sc0S6FGQf4ttshsiS+Pea5AygbwEnuozbAsrfMqkSORRvU74fI0tS4CRtujATLEpHt4QKiViSjPtnea8DK7QoTkDiy+EjeocyCNQezsqoadJ1a/50hzLnJ//PXoBKsu0tLazs+ARjuswHZA+oOFqzUQJi4hT70zVJipOyJFzs2vBr/bWuV5gH/OpTr2P6F3jft8DuQoRneYBFvrRs5vvnOaTw+Eglx65jFvbg9c0pxKDh4HBVFi9HUE6DJGYri7AJJO/37D0irp6OMTKDiZQ5ooBf+avoQS2zFrEUZIe4uyKxMeh//0qjeedYvRKU4OwhFQ/1AywJpk/pW4/xGmimsISSWWyajNItPia/9rgGGCXYdQmLYzHdUTg6UEMl1O/edxulkGfnqGT6y7ByERr9wItkzjj0q+XYx5jpY0fg8PE5ay4s7LPQEI+p9JCSIbslif4hj1K7Ph/JGiHm3aFg3lj9QdxfgUmARKKnS4te2yboEw8owUDhnjy2mo/di1Xn+Rsusl9KwleK7DBd0fgoxBqPm+ovcOHxvjU5yEe68rMRBx0OQJNtvxiJMjzquRAci97B8E3t76xfQYTj/t36EW7pz9kDUVzkO4BrAmZeeg5Eclb6cDRa1KsS27lr1nkH1MPJhfSYMokpsdfal6Eeq5eF+tZnAxbXE0bVokXmkDob2JOFrTaejua7WOGkNPtuC/nM9V5NQQRJpaQcw4G9ShMG/plndLexVcB1UJmaafNqVFcxIYsZ2+aRqEuAGRgtSE525t+F906AHu/MCjpSpJrJGrCBsaEtKqagS3qfg35BGNAWlRwGhaBbQccPsxyoXOKUFRDgNygzJiQyJvGl3UhaUJshApHPYRWZPRYRi1ClHbtMPrT5T2ta5d+s9MXXnF7YK9WhiErxkDIMXUPYzrITeX65us7JjLdRyDj4HscxYIs0i40p2+94jTDDhlg4F+W3POet0ZUwUST+8OWeAHM0mcIPkuuc8aJvBUeLua8j0nZjPxrG4YfpLeXm2awkiZdCuzYiDc+WO222tNYu5Bc2AdHl2cTmjKYuHZgOTkqJfAIdaBUMKjj/c3wDFlDntZFOv6OJKV3zH5fhF7I78W7YbVQIAkPhymdKkl2yzjICsmnWMF+/v+HpRc7bcoHZK/Bg86DnzhxyoUp6qF569SdFBg2dnM1BHn4rS2K3a1Crt4HoUfnY9uvyYFTjFHMhBqvdMqivEfFeiYral88Bs7buQeUbrPEq4v+yax31Gr+YcbnodDsWjWUBywUCQMhQa2ZQzM/XD361SJTM5tjUFXDow3Vs1aDfZ9PoFpwb73acgi+SXquAfi1RLR+sZ0PNDeslLADZF+OckDoHYwHc/+AsRvSq4n1GRvLpcZhxPOMXdUqZTGTkmhC8+M9IS8RkTPlhhs/mSdXp3gJ3dhpAYkb7tlmvX9gHGmWQgnu4=|ysx/NjMZjqu+j7H3P1G+SDZAc8c+M2x6uDD1w8VDMMk=", + encapsulated_key: + "2.ohB9QRqyPhd4BBvqGsjxXw==|D/NTfz7fj4q8yG8KI2x/1JKpY2dSll8mefOxHvO7LCVF8l3iovn3q4S+ZYzIvU9CT8KKyirdx5GdBHUk744rcTyE+sjweJXwzVKXPcNx07o6UsdziYAQzSIpZIs2N7arUeoD8VyIegAiRUaJp/g8q7K9++5o3T2/RRD/NGvDrraBrQbCtQs3Gl8nYJ/c6Ve5MPtej29kKREo2o8SMCFkr6j8UNdgbXwIxujC8H6pLZkMj1uCdbvFoSQENCkybTv+DJr8BCcPsTZ7ov11LmUGuq4EvOGseQnZxwJD4BGcrl3iSLX62vUg/6NHFbkgskjSbOrMMO4rvMIBmQkBjvKxzf9IRUASEqTAJH7gv9TKGED0VzJrYgFL9bplOU+puzDcaY2scNID5fMnSB9384CvmqstQL1TIHh03Jkqj8o5WNQw/ZHTcY2RUfeWdOsLn30JROht3bkxLmqbphTttI8XXVIH2pWinztMlQ66SgKU7TaX1qHm5FS4CLasP9IH541S/Syd+kbUZ1/diLYIKu7PyQ==|BLGTKJT50o9NBPJ2XeQG9sOeFqVJlIezauvi1mOt/XU=", + } as RotateableKeyset; + + rotatableKeySet = new RotateableKeySet( + new EncString(sdkRotateableKeyset.encapsulated_key), + new EncString(sdkRotateableKeyset.public_key), + new EncString(sdkRotateableKeyset.private_key), + ); }); describe("register input validation", () => { @@ -99,7 +149,99 @@ describe("DefaultOpaqueKeyExchangeService", () => { }); }); - // TODO: test registration process - // it("registers a user with OPAQUE with the provided master password, user key, and cipher config", async () => {}); + it("registers a user with OPAQUE with the provided master password, user key, and cipher config", async () => { + // Arrange + sdkCryptoClient.opaque_register_start = jest + .fn() + .mockReturnValue(clientRegistrationStartResult); + + jest.spyOn(Utils, "fromBufferToB64").mockImplementation((input) => { + if (input === clientRegistrationStartResult.registration_request) { + return registrationRequestB64; + } + + if (input === clientRegistrationFinishResult.registration_upload) { + return registrationUploadB64; + } + + return ""; + }); + + opaqueKeyExchangeApiService.registrationStart.mockResolvedValue(registrationStartResponse); + + jest.spyOn(Utils, "fromB64ToArray").mockImplementationOnce(() => { + return registrationResponseArray; + }); + + sdkCryptoClient.opaque_register_finish = jest + .fn() + .mockReturnValue(clientRegistrationFinishResult); + + sdkCryptoClient.create_rotateablekeyset_from_exportkey = jest + .fn() + .mockReturnValue(sdkRotateableKeyset); + + // Act + const result = await service.register(masterPassword, userKey, opaqueCipherConfig); + + // Assert + expect(sdkCryptoClient.opaque_register_start).toHaveBeenCalledWith( + masterPassword, + opaqueCipherConfig.toSdkConfig(), + ); + + expect(opaqueKeyExchangeApiService.registrationStart).toHaveBeenCalledWith( + expect.objectContaining({ + registrationRequest: registrationRequestB64, + cipherConfiguration: opaqueCipherConfig, + }), + ); + + expect(sdkCryptoClient.opaque_register_finish).toHaveBeenCalledWith( + masterPassword, + opaqueCipherConfig.toSdkConfig(), + registrationResponseArray, + clientRegistrationStartResult.state, + ); + + expect(sdkCryptoClient.create_rotateablekeyset_from_exportkey).toHaveBeenCalledWith( + clientRegistrationFinishResult.export_key, + userKey.key, + ); + + expect(opaqueKeyExchangeApiService.registrationFinish).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: registrationStartResponse.sessionId, + registrationUpload: registrationUploadB64, + keySet: rotatableKeySet, + }), + ); + + expect(result).toBe(registrationStartResponse.sessionId); + }); }); + + describe("setRegistrationActive", () => { + let sessionId: OpaqueSessionId; + + beforeEach(() => { + sessionId = "sessionId" as OpaqueSessionId; + }); + + it("sets the registration as active", async () => { + // Act + await service.setRegistrationActive(sessionId); + + // Assert + expect(opaqueKeyExchangeApiService.setRegistrationActive).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId, + }), + ); + }); + }); + + // TODO: test login + + // TODO: test decryptUserKeyWithExportKey });