You've already forked matrix-js-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-js-sdk.git
				synced 2025-11-03 01:53:18 +03:00 
			
		
		
		
	* tests: Cross-signing keys support in `E2EKeyReceiver` Have `E2EKeyReceiver` collect uploaded cross-signing keys, so that they can be returned by `E2EKeyResponder`. * tests: Signature upload support in `E2EKeyReceiver` Have `E2EKeyReceiver` collect uploaded device signatures, so that they can be returned by `E2EKeyResponder`. * tests: Implement `E2EOTKClaimResponder` class A new test helper, which intercepts `/keys/claim`, allowing clients under test to claim OTKs uploaded by other devices. * Expose experimental settings for encrypted history sharing Add options to `MatrixClient.invite` and `MatrixClient.joinRoom` to share and accept encrypted history on invite, per MSC4268. * Clarify pre-join-membership logic * Improve tests * Update spec/integ/crypto/cross-signing.spec.ts Co-authored-by: Hubert Chathi <hubertc@matrix.org> --------- Co-authored-by: Hubert Chathi <hubertc@matrix.org>
		
			
				
	
	
		
			240 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			240 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/*
 | 
						|
Copyright 2025 The Matrix.org Foundation C.I.C.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
import "fake-indexeddb/auto";
 | 
						|
import fetchMock from "fetch-mock-jest";
 | 
						|
import mkDebug from "debug";
 | 
						|
 | 
						|
import {
 | 
						|
    createClient,
 | 
						|
    DebugLogger,
 | 
						|
    EventType,
 | 
						|
    type IContent,
 | 
						|
    KnownMembership,
 | 
						|
    type MatrixClient,
 | 
						|
    MsgType,
 | 
						|
} from "../../../src";
 | 
						|
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver.ts";
 | 
						|
import { SyncResponder } from "../../test-utils/SyncResponder.ts";
 | 
						|
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints.ts";
 | 
						|
import { getSyncResponse, mkEventCustom, syncPromise } from "../../test-utils/test-utils.ts";
 | 
						|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder.ts";
 | 
						|
import { flushPromises } from "../../test-utils/flushPromises.ts";
 | 
						|
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
 | 
						|
import { escapeRegExp } from "../../../src/utils.ts";
 | 
						|
 | 
						|
const debug = mkDebug("matrix-js-sdk:history-sharing");
 | 
						|
 | 
						|
// load the rust library. This can take a few seconds on a slow GH worker.
 | 
						|
beforeAll(async () => {
 | 
						|
    // eslint-disable-next-line @typescript-eslint/no-require-imports
 | 
						|
    const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
 | 
						|
    await RustSdkCryptoJs.initAsync();
 | 
						|
}, 10000);
 | 
						|
 | 
						|
afterEach(() => {
 | 
						|
    // reset fake-indexeddb after each test, to make sure we don't leak connections
 | 
						|
    // cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
 | 
						|
    // eslint-disable-next-line no-global-assign
 | 
						|
    indexedDB = new IDBFactory();
 | 
						|
});
 | 
						|
 | 
						|
const ROOM_ID = "!room:example.com";
 | 
						|
const ALICE_HOMESERVER_URL = "https://alice-server.com";
 | 
						|
const BOB_HOMESERVER_URL = "https://bob-server.com";
 | 
						|
 | 
						|
async function createAndInitClient(homeserverUrl: string, userId: string) {
 | 
						|
    mockInitialApiRequests(homeserverUrl, userId);
 | 
						|
 | 
						|
    const client = createClient({
 | 
						|
        baseUrl: homeserverUrl,
 | 
						|
        userId: userId,
 | 
						|
        accessToken: "akjgkrgjs",
 | 
						|
        deviceId: "xzcvb",
 | 
						|
        logger: new DebugLogger(mkDebug(`matrix-js-sdk:${userId}`)),
 | 
						|
    });
 | 
						|
 | 
						|
    await client.initRustCrypto({ cryptoDatabasePrefix: userId });
 | 
						|
    await client.startClient();
 | 
						|
    await client.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
 | 
						|
    return client;
 | 
						|
}
 | 
						|
 | 
						|
describe("History Sharing", () => {
 | 
						|
    let aliceClient: MatrixClient;
 | 
						|
    let aliceSyncResponder: SyncResponder;
 | 
						|
    let bobClient: MatrixClient;
 | 
						|
    let bobSyncResponder: SyncResponder;
 | 
						|
 | 
						|
    beforeEach(async () => {
 | 
						|
        // anything that we don't have a specific matcher for silently returns a 404
 | 
						|
        fetchMock.catch(404);
 | 
						|
        fetchMock.config.warnOnFallback = false;
 | 
						|
        mockSetupCrossSigningRequests();
 | 
						|
 | 
						|
        const aliceId = "@alice:localhost";
 | 
						|
        const bobId = "@bob:xyz";
 | 
						|
 | 
						|
        const aliceKeyReceiver = new E2EKeyReceiver(ALICE_HOMESERVER_URL, "alice-");
 | 
						|
        const aliceKeyResponder = new E2EKeyResponder(ALICE_HOMESERVER_URL);
 | 
						|
        const aliceKeyClaimResponder = new E2EOTKClaimResponder(ALICE_HOMESERVER_URL);
 | 
						|
        aliceSyncResponder = new SyncResponder(ALICE_HOMESERVER_URL);
 | 
						|
 | 
						|
        const bobKeyReceiver = new E2EKeyReceiver(BOB_HOMESERVER_URL, "bob-");
 | 
						|
        const bobKeyResponder = new E2EKeyResponder(BOB_HOMESERVER_URL);
 | 
						|
        bobSyncResponder = new SyncResponder(BOB_HOMESERVER_URL);
 | 
						|
 | 
						|
        aliceKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
 | 
						|
        aliceKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
 | 
						|
        bobKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
 | 
						|
        bobKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
 | 
						|
 | 
						|
        aliceClient = await createAndInitClient(ALICE_HOMESERVER_URL, aliceId);
 | 
						|
        bobClient = await createAndInitClient(BOB_HOMESERVER_URL, bobId);
 | 
						|
 | 
						|
        aliceKeyClaimResponder.addKeyReceiver(bobId, bobClient.deviceId!, bobKeyReceiver);
 | 
						|
 | 
						|
        aliceSyncResponder.sendOrQueueSyncResponse({});
 | 
						|
        await syncPromise(aliceClient);
 | 
						|
 | 
						|
        bobSyncResponder.sendOrQueueSyncResponse({});
 | 
						|
        await syncPromise(bobClient);
 | 
						|
    });
 | 
						|
 | 
						|
    test("Room keys are successfully shared on invite", async () => {
 | 
						|
        // Alice is in an encrypted room
 | 
						|
        const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
 | 
						|
        aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
 | 
						|
        await syncPromise(aliceClient);
 | 
						|
 | 
						|
        // ... and she sends an event
 | 
						|
        const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
 | 
						|
        await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hi!" });
 | 
						|
        const sentMessage = await msgProm;
 | 
						|
        debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
 | 
						|
 | 
						|
        // Now, Alice invites Bob
 | 
						|
        const uploadProm = new Promise<Uint8Array>((resolve) => {
 | 
						|
            fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (url, request) => {
 | 
						|
                const body = request.body as Uint8Array;
 | 
						|
                debug(`Alice uploaded blob of length ${body.length}`);
 | 
						|
                resolve(body);
 | 
						|
                return { content_uri: "mxc://alice-server/here" };
 | 
						|
            });
 | 
						|
        });
 | 
						|
        const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
 | 
						|
        // POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
 | 
						|
        fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
 | 
						|
        await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
 | 
						|
        const uploadedBlob = await uploadProm;
 | 
						|
        const sentToDeviceRequest = await toDeviceMessageProm;
 | 
						|
        debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
 | 
						|
        const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
 | 
						|
        expect(bobToDeviceMessage).toBeDefined();
 | 
						|
 | 
						|
        // Bob receives the to-device event and the room invite
 | 
						|
        const inviteEvent = mkEventCustom({
 | 
						|
            type: "m.room.member",
 | 
						|
            sender: aliceClient.getSafeUserId(),
 | 
						|
            state_key: bobClient.getSafeUserId(),
 | 
						|
            content: { membership: KnownMembership.Invite },
 | 
						|
        });
 | 
						|
        bobSyncResponder.sendOrQueueSyncResponse({
 | 
						|
            rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
 | 
						|
            to_device: {
 | 
						|
                events: [
 | 
						|
                    {
 | 
						|
                        type: "m.room.encrypted",
 | 
						|
                        sender: aliceClient.getSafeUserId(),
 | 
						|
                        content: bobToDeviceMessage,
 | 
						|
                    },
 | 
						|
                ],
 | 
						|
            },
 | 
						|
        });
 | 
						|
        await syncPromise(bobClient);
 | 
						|
 | 
						|
        const room = bobClient.getRoom(ROOM_ID);
 | 
						|
        expect(room).toBeTruthy();
 | 
						|
        expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
 | 
						|
 | 
						|
        fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
 | 
						|
            room_id: ROOM_ID,
 | 
						|
        });
 | 
						|
        fetchMock.getOnce(
 | 
						|
            `begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
 | 
						|
            { body: uploadedBlob },
 | 
						|
            { sendAsJson: false },
 | 
						|
        );
 | 
						|
        await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
 | 
						|
 | 
						|
        // Bob receives, should be able to decrypt, the megolm message
 | 
						|
        const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
 | 
						|
        bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
 | 
						|
            mkEventCustom({
 | 
						|
                type: "m.room.encrypted",
 | 
						|
                sender: aliceClient.getSafeUserId(),
 | 
						|
                content: sentMessage,
 | 
						|
                event_id: "$event_id",
 | 
						|
            }) as any,
 | 
						|
        );
 | 
						|
        bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
 | 
						|
        await syncPromise(bobClient);
 | 
						|
 | 
						|
        const bobRoom = bobClient.getRoom(ROOM_ID);
 | 
						|
        const event = bobRoom!.getLastLiveEvent()!;
 | 
						|
        expect(event.getId()).toEqual("$event_id");
 | 
						|
        await event.getDecryptionPromise();
 | 
						|
        expect(event.getType()).toEqual("m.room.message");
 | 
						|
        expect(event.getContent().body).toEqual("Hi!");
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(async () => {
 | 
						|
        bobClient.stopClient();
 | 
						|
        aliceClient.stopClient();
 | 
						|
        await flushPromises();
 | 
						|
    });
 | 
						|
});
 | 
						|
 | 
						|
function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IContent> {
 | 
						|
    return new Promise<IContent>((resolve) => {
 | 
						|
        fetchMock.putOnce(
 | 
						|
            new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/rooms/[^/]*/send/${escapeRegExp(msgtype)}/`),
 | 
						|
            (url, request) => {
 | 
						|
                const content = JSON.parse(request.body as string);
 | 
						|
                resolve(content);
 | 
						|
                return { event_id: "$event_id" };
 | 
						|
            },
 | 
						|
            { name: "sendRoomEvent" },
 | 
						|
        );
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
function expectSendToDeviceMessage(
 | 
						|
    homeserverUrl: string,
 | 
						|
    msgtype: string,
 | 
						|
): Promise<Record<string, Record<string, object>>> {
 | 
						|
    return new Promise((resolve) => {
 | 
						|
        fetchMock.putOnce(
 | 
						|
            new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/sendToDevice/${escapeRegExp(msgtype)}/`),
 | 
						|
            (url: string, opts: RequestInit) => {
 | 
						|
                const body = JSON.parse(opts.body as string);
 | 
						|
                resolve(body.messages);
 | 
						|
                return {};
 | 
						|
            },
 | 
						|
        );
 | 
						|
    });
 | 
						|
}
 |