From c36bfc821c18d90cceb46fe6dd49732ca69bfc0a Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 3 Aug 2022 12:16:48 -0400 Subject: [PATCH] Add support for sending user-defined encrypted to-device messages (#2528) * Add support for sending user-defined encrypted to-device messages This is a port of the same change from the robertlong/group-call branch. * Fix tests * Expose the method in MatrixClient * Fix a code smell * Fix types * Test the MatrixClient method * Fix some types in Crypto test suite * Test the Crypto method * Fix tests * Upgrade matrix-mock-request * Move useRealTimers to afterEach --- package.json | 2 +- spec/unit/crypto.spec.ts | 130 +++++++++++++++++++-- spec/unit/crypto/algorithms/megolm.spec.ts | 10 ++ spec/unit/matrix-client.spec.ts | 16 +++ src/client.ts | 32 ++++- src/crypto/algorithms/megolm.ts | 95 +++------------ src/crypto/index.ts | 104 +++++++++++++++++ yarn.lock | 21 ++-- 8 files changed, 303 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 366608eb2..ece642f4a 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "jest-localstorage-mock": "^2.4.6", "jest-sonar-reporter": "^2.0.0", "jsdoc": "^3.6.6", - "matrix-mock-request": "^2.1.1", + "matrix-mock-request": "^2.1.2", "rimraf": "^3.0.2", "terser": "^5.5.1", "tsify": "^5.0.2", diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index b579b7f38..19217cdda 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -2,6 +2,7 @@ import '../olm-loader'; // eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; +import { MatrixClient } from "../../src/client"; import { Crypto } from "../../src/crypto"; import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; import { MockStorageApi } from "../MockStorageApi"; @@ -64,6 +65,10 @@ describe("Crypto", function() { return Olm.init(); }); + afterEach(() => { + jest.useRealTimers(); + }); + it("Crypto exposes the correct olm library version", function() { expect(Crypto.getOlmVersion()[0]).toEqual(3); }); @@ -225,8 +230,8 @@ describe("Crypto", function() { }); describe('Key requests', function() { - let aliceClient; - let bobClient; + let aliceClient: MatrixClient; + let bobClient: MatrixClient; beforeEach(async function() { aliceClient = (new TestClient( @@ -313,7 +318,7 @@ describe("Crypto", function() { expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - const cryptoStore = bobClient.cryptoStore; + const cryptoStore = bobClient.crypto.cryptoStore; const eventContent = events[0].getWireContent(); const senderKey = eventContent.sender_key; const sessionId = eventContent.session_id; @@ -383,9 +388,9 @@ describe("Crypto", function() { const ksEvent = await keyshareEventForEvent(aliceClient, event, 1); ksEvent.getContent().sender_key = undefined; // test - bobClient.crypto.addInboundGroupSession = jest.fn(); + bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn(); await bobDecryptor.onRoomKeyEvent(ksEvent); - expect(bobClient.crypto.addInboundGroupSession).not.toHaveBeenCalled(); + expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); }); it("creates a new keyshare request if we request a keyshare", async function() { @@ -401,7 +406,7 @@ describe("Crypto", function() { }, }); await aliceClient.cancelAndResendEventRoomKeyRequest(event); - const cryptoStore = aliceClient.cryptoStore; + const cryptoStore = aliceClient.crypto.cryptoStore; const roomKeyRequestBody = { algorithm: olmlib.MEGOLM_ALGORITHM, room_id: "!someroom", @@ -425,7 +430,8 @@ describe("Crypto", function() { }, }); // replace Alice's sendToDevice function with a mock - aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined); + const aliceSendToDevice = jest.fn().mockResolvedValue(undefined); + aliceClient.sendToDevice = aliceSendToDevice; aliceClient.startClient(); // make a room key request, and record the transaction ID for the @@ -434,11 +440,12 @@ describe("Crypto", function() { // key requests get queued until the sync has finished, but we don't // let the client set up enough for that to happen, so gut-wrench a bit // to force it to send now. + // @ts-ignore aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests(); jest.runAllTimers(); await Promise.resolve(); - expect(aliceClient.sendToDevice).toBeCalledTimes(1); - const txnId = aliceClient.sendToDevice.mock.calls[0][2]; + expect(aliceSendToDevice).toBeCalledTimes(1); + const txnId = aliceSendToDevice.mock.calls[0][2]; // give the room key request manager time to update the state // of the request @@ -451,8 +458,8 @@ describe("Crypto", function() { // cancelAndResend will call sendToDevice twice: // the first call to sendToDevice will be the cancellation // the second call to sendToDevice will be the key request - expect(aliceClient.sendToDevice).toBeCalledTimes(3); - expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId); + expect(aliceSendToDevice).toBeCalledTimes(3); + expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId); }); }); @@ -480,4 +487,105 @@ describe("Crypto", function() { client.stopClient(); }); }); + + describe("encryptAndSendToDevices", () => { + let client: TestClient; + let ensureOlmSessionsForDevices: jest.SpiedFunction; + let encryptMessageForDevice: jest.SpiedFunction; + const payload = { hello: "world" }; + let encryptedPayload: object; + + beforeEach(async () => { + ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices"); + ensureOlmSessionsForDevices.mockResolvedValue({}); + encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); + encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => { + result.plaintext = JSON.stringify(payload); + }); + + client = new TestClient("@alice:example.org", "aliceweb"); + await client.client.initCrypto(); + + encryptedPayload = { + algorithm: "m.olm.v1.curve25519-aes-sha2", + sender_key: client.client.crypto.olmDevice.deviceCurve25519Key, + ciphertext: { plaintext: JSON.stringify(payload) }, + }; + }); + + afterEach(async () => { + ensureOlmSessionsForDevices.mockRestore(); + encryptMessageForDevice.mockRestore(); + await client.stop(); + }); + + it("encrypts and sends to devices", async () => { + client.httpBackend + .when("PUT", "/sendToDevice/m.room.encrypted", { + messages: { + "@bob:example.org": { + bobweb: encryptedPayload, + bobmobile: encryptedPayload, + }, + "@carol:example.org": { + caroldesktop: encryptedPayload, + }, + }, + }) + .respond(200, {}); + + await Promise.all([ + client.client.encryptAndSendToDevices( + [ + { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }, + { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobmobile") }, + { userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") }, + ], + payload, + ), + client.httpBackend.flushAllExpected(), + ]); + }); + + it("sends nothing to devices that couldn't be encrypted to", async () => { + encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => { + // Refuse to encrypt to Carol's desktop device + if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; + result.plaintext = JSON.stringify(payload); + }); + + client.httpBackend + .when("PUT", "/sendToDevice/m.room.encrypted", { + // Carol is nowhere to be seen + messages: { "@bob:example.org": { bobweb: encryptedPayload } }, + }) + .respond(200, {}); + + await Promise.all([ + client.client.encryptAndSendToDevices( + [ + { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }, + { userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") }, + ], + payload, + ), + client.httpBackend.flushAllExpected(), + ]); + }); + + it("no-ops if no devices can be encrypted to", async () => { + // Refuse to encrypt to anybody + encryptMessageForDevice.mockResolvedValue(undefined); + + // Get the room keys version request out of the way + client.httpBackend.when("GET", "/room_keys/version").respond(404, {}); + await client.httpBackend.flush("/room_keys/version", 1); + + await client.client.encryptAndSendToDevices( + [{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }], + payload, + ); + client.httpBackend.verifyNoOutstandingRequests(); + }); + }); }); diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 3b39dcd86..9aa3c5c78 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -360,6 +360,16 @@ describe("MegolmDecryption", function() { rotation_period_ms: rotationPeriodMs, }, }); + + // Splice the real method onto the mock object as megolm uses this method + // on the crypto class in order to encrypt / start sessions + // @ts-ignore Mock + mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices; + // @ts-ignore Mock + mockCrypto.olmDevice = olmDevice; + // @ts-ignore Mock + mockCrypto.baseApis = mockBaseApis; + mockRoom = { getEncryptionTargetMembers: jest.fn().mockReturnValue( [{ userId: "@alice:home.server" }], diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index fbe8c67d7..db3377501 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -27,6 +27,7 @@ import { UNSTABLE_MSC3089_TREE_SUBTYPE, } from "../../src/@types/event"; import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib"; +import { Crypto } from "../../src/crypto"; import { EventStatus, MatrixEvent } from "../../src/models/event"; import { Preset } from "../../src/@types/partials"; import { ReceiptType } from "../../src/@types/read_receipts"; @@ -1297,4 +1298,19 @@ describe("MatrixClient", function() { expect(result!.aliases).toEqual(response.aliases); }); }); + + describe("encryptAndSendToDevices", () => { + it("throws an error if crypto is unavailable", () => { + client.crypto = undefined; + expect(() => client.encryptAndSendToDevices([], {})).toThrow(); + }); + + it("is an alias for the crypto method", async () => { + client.crypto = testUtils.mock(Crypto, "Crypto"); + const deviceInfos = []; + const payload = {}; + await client.encryptAndSendToDevices(deviceInfos, payload); + expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); + }); + }); }); diff --git a/src/client.ts b/src/client.ts index 69c8ffdc0..69daf9028 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,9 +40,11 @@ import { sleep } from './utils'; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; +import { IEncryptAndSendToDevicesResult } from "./crypto"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; -import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice"; +import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice"; +import { IOlmDevice } from "./crypto/algorithms/megolm"; import { TypedReEmitter } from './ReEmitter'; import { IRoomEncryption, RoomList } from './crypto/RoomList'; import { logger } from './logger'; @@ -208,7 +210,7 @@ const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes interface IExportedDevice { - olmDevice: IOlmDevice; + olmDevice: IExportedOlmDevice; userId: string; deviceId: string; } @@ -936,7 +938,7 @@ export class MatrixClient extends TypedEventEmitter; - protected exportedOlmDeviceToImport: IOlmDevice; + protected exportedOlmDeviceToImport: IExportedOlmDevice; protected txnCtr = 0; protected mediaHandler = new MediaHandler(this); protected pendingEventEncryption = new Map>(); @@ -2558,6 +2560,30 @@ export class MatrixClient extends TypedEventEmitter} Promise which + * resolves once the message has been encrypted and sent to the given + * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * of the successfully sent messages. + */ + public encryptAndSendToDevices( + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); + } + /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 9233c4deb..395c834c5 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -22,7 +22,6 @@ limitations under the License. import { logger } from '../../logger'; import * as olmlib from "../olmlib"; -import { EventType } from '../../@types/event'; import { DecryptionAlgorithm, DecryptionError, @@ -38,7 +37,6 @@ import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; import { MatrixEvent } from "../.."; import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; -import { ToDeviceBatch, ToDeviceMessage } from '../../models/ToDeviceMessage'; // determine whether the key can be shared with invitees export function isRoomSharedHistory(room: Room): boolean { @@ -611,87 +609,22 @@ class MegolmEncryption extends EncryptionAlgorithm { userDeviceMap: IOlmDevice[], payload: IPayload, ): Promise { - const toDeviceBatch: ToDeviceBatch = { - eventType: EventType.RoomMessageEncrypted, - batch: [], - }; - - // Map from userId to a map of deviceId to deviceInfo - const deviceInfoByUserIdAndDeviceId = new Map>(); - - const promises: Promise[] = []; - for (let i = 0; i < userDeviceMap.length; i++) { - const encryptedContent: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {}, - }; - const val = userDeviceMap[i]; - const userId = val.userId; - const deviceInfo = val.deviceInfo; - const deviceId = deviceInfo.deviceId; - - // Assign to temp value to make type-checking happy - let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId); - - if (userIdDeviceInfo === undefined) { - userIdDeviceInfo = new Map(); - - deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo); - } - - // We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId] - userIdDeviceInfo.set(deviceId, deviceInfo); - - toDeviceBatch.batch.push({ - userId, - deviceId, - payload: encryptedContent, - }); - - promises.push( - olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this.userId, - this.deviceId, - this.olmDevice, - userId, - deviceInfo, - payload, - ), - ); - } - - return Promise.all(promises).then(() => { - // prune out any devices that encryptMessageForDevice could not encrypt for, - // in which case it will have just not added anything to the ciphertext object. - // There's no point sending messages to devices if we couldn't encrypt to them, - // since that's effectively a blank message. - const prunedBatch: ToDeviceMessage[] = []; + return this.crypto.encryptAndSendToDevices( + userDeviceMap, + payload, + ).then(({ toDeviceBatch, deviceInfoByUserIdAndDeviceId }) => { + // store that we successfully uploaded the keys of the current slice for (const msg of toDeviceBatch.batch) { - if (Object.keys(msg.payload.ciphertext).length > 0) { - prunedBatch.push(msg); - } else { - logger.log( - "No ciphertext for device " + - msg.userId + ":" + msg.deviceId + ": pruning", - ); - } + session.markSharedWithDevice( + msg.userId, + msg.deviceId, + deviceInfoByUserIdAndDeviceId.get(msg.userId).get(msg.deviceId).getIdentityKey(), + chainIndex, + ); } - - toDeviceBatch.batch = prunedBatch; - - return this.baseApis.queueToDevice(toDeviceBatch).then(() => { - // store that we successfully uploaded the keys of the current slice - for (const msg of toDeviceBatch.batch) { - session.markSharedWithDevice( - msg.userId, - msg.deviceId, - deviceInfoByUserIdAndDeviceId.get(msg.userId).get(msg.deviceId).getIdentityKey(), - chainIndex, - ); - } - }); + }).catch((error) => { + logger.error("failed to encryptAndSendToDevices", error); + throw error; }); } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 4438edc8a..ff5ab43fe 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -23,9 +23,11 @@ limitations under the License. import anotherjson from "another-json"; +import { EventType } from "../@types/event"; import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; import { IExportedDevice, OlmDevice } from "./OlmDevice"; +import { IOlmDevice } from "./algorithms/megolm"; import * as olmlib from "./olmlib"; import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfo, IDevice } from "./deviceinfo"; @@ -68,6 +70,7 @@ import { IStore } from "../store"; import { Room, RoomEvent } from "../models/room"; import { RoomMember, RoomMemberEvent } from "../models/room-member"; import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event"; +import { ToDeviceBatch } from "../models/ToDeviceMessage"; import { ClientEvent, ICrossSigningKey, @@ -201,6 +204,19 @@ export interface IRequestsMap { setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void; } +/* eslint-disable camelcase */ +export interface IEncryptedContent { + algorithm: string; + sender_key: string; + ciphertext: Record; +} +/* eslint-enable camelcase */ + +export interface IEncryptAndSendToDevicesResult { + toDeviceBatch: ToDeviceBatch; + deviceInfoByUserIdAndDeviceId: Map>; +} + export enum CryptoEvent { DeviceVerificationChanged = "deviceVerificationChanged", UserTrustStatusChanged = "userTrustStatusChanged", @@ -3100,6 +3116,94 @@ export class Crypto extends TypedEventEmitter} Promise which + * resolves once the message has been encrypted and sent to the given + * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * of the successfully sent messages. + */ + public async encryptAndSendToDevices( + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + const toDeviceBatch: ToDeviceBatch = { + eventType: EventType.RoomMessageEncrypted, + batch: [], + }; + const deviceInfoByUserIdAndDeviceId = new Map>(); + + try { + await Promise.all(userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { + const deviceId = deviceInfo.deviceId; + const encryptedContent: IEncryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + + // Assign to temp value to make type-checking happy + let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId); + + if (userIdDeviceInfo === undefined) { + userIdDeviceInfo = new Map(); + deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo); + } + + // We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId] + userIdDeviceInfo.set(deviceId, deviceInfo); + + toDeviceBatch.batch.push({ + userId, + deviceId, + payload: encryptedContent, + }); + + await olmlib.ensureOlmSessionsForDevices( + this.olmDevice, + this.baseApis, + { [userId]: [deviceInfo] }, + ); + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this.userId, + this.deviceId, + this.olmDevice, + userId, + deviceInfo, + payload, + ); + })); + + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + toDeviceBatch.batch = toDeviceBatch.batch.filter(msg => { + if (Object.keys(msg.payload.ciphertext).length > 0) { + return true; + } else { + logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); + return false; + } + }); + + try { + await this.baseApis.queueToDevice(toDeviceBatch); + return { toDeviceBatch, deviceInfoByUserIdAndDeviceId }; + } catch (e) { + logger.error("sendToDevice failed", e); + throw e; + } + } catch (e) { + logger.error("encryptAndSendToDevices promises failed", e); + throw e; + } + } + private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { try { this.onRoomMembership(event, member, oldMembership); diff --git a/yarn.lock b/yarn.lock index a8d843cce..fc673f365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,7 +1307,6 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": version "3.2.12" - uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": @@ -1438,9 +1437,9 @@ "@octokit/openapi-types" "^12.10.0" "@sinclair/typebox@^0.24.1": - version "0.24.20" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" - integrity sha512-kVaO5aEFZb33nPMTZBxiPEkY+slxiPtqC7QX8f9B3eGOMBvEfuMfxp9DSTTCsRJPumPKjrge4yagyssO4q6qzQ== + version "0.24.26" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.26.tgz#84f9e8c1d93154e734a7947609a1dc7c7a81cc22" + integrity sha512-1ZVIyyS1NXDRVT8GjWD5jULjhDyM3IsIHef2VGUMdnWOlX2tkPjyEX/7K0TGSH2S8EaPhp1ylFdjSjUGQ+gecg== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -1581,9 +1580,9 @@ integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/node@*": - version "18.6.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5" - integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg== + version "18.6.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.3.tgz#4e4a95b6fe44014563ceb514b2598b3e623d1c98" + integrity sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg== "@types/node@16": version "16.11.45" @@ -4802,10 +4801,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -matrix-mock-request@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.1.tgz#a8fc03a2816464bb95445df4cc8885ac36786b23" - integrity sha512-CxdaUPRVB4o8JxTBMASstS2loRe+hlqeJu0Q7yyS1r36LkSSo/KAP4AuomsqxuKqaqYYnEJFJzkG0gOhxV7aqA== +matrix-mock-request@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.2.tgz#11e38ed1233dced88a6f2bfba1684d5c5b3aa2c2" + integrity sha512-/OXCIzDGSLPJ3fs+uzDrtaOHI/Sqp4iEuniRn31U8S06mPXbvAnXknHqJ4c6A/KVwJj/nPFbGXpK4wPM038I6A== dependencies: expect "^28.1.0"