You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
crypto: Add new ClientEvent.ReceivedToDeviceMessage
with proper OlmEncryptionInfo
support (#4891)
* crypto: Add new ClientEvent.ReceivedToDeviceMessage refactor rename ProcessedToDeviceEvent to ReceivedToDeviceEvent * fix: Restore legacy isEncrypted() for to-device messages * Update test for new preprocessToDeviceMessages API * quick fix on doc * quick update docs and renaming * review: Better doc and names for OlmEncryptionInfo * review: Remove IToDeviceMessage alias and only keep IToDeviceEvent * review: improve comments of processToDeviceMessages * review: pass up encrypted event when no crypto callbacks * review: use single payload for ReceivedToDeviceMessage * fix linter * review: minor comment update
This commit is contained in:
@ -17,13 +17,22 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { createClient, type MatrixClient } from "../../../src";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
type IToDeviceEvent,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@ -43,6 +52,8 @@ describe("to-device-messages", () => {
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
@ -59,8 +70,8 @@ describe("to-device-messages", () => {
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
const syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
@ -149,4 +160,111 @@ describe("to-device-messages", () => {
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
|
||||
describe("receive to-device-messages", () => {
|
||||
it("Should receive decrypted to-device message via ClientEvent", async () => {
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
const toDeviceEvent = encryptOlmEvent({
|
||||
sender: "@bob:xyz",
|
||||
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
|
||||
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
|
||||
p2pSession: p2pSession,
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
|
||||
plaincontent: {
|
||||
body: "foo",
|
||||
},
|
||||
plaintype: "m.test.type",
|
||||
});
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
expect(toDeviceEvent.type).toBe("m.room.encrypted");
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
expect(encryptionInfo).not.toBeNull();
|
||||
expect(encryptionInfo!.senderVerified).toBe(false);
|
||||
expect(encryptionInfo!.sender).toBe("@bob:xyz");
|
||||
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(true);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
|
||||
it("Should receive clear to-device message via ClientEvent", async () => {
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceEvent: IToDeviceEvent = {
|
||||
sender: "@bob:xyz",
|
||||
type: "m.test.type",
|
||||
content: {
|
||||
body: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
// When the message is not encrypted, we don't have the encryptionInfo.
|
||||
expect(encryptionInfo).toBeNull();
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(false);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -551,11 +551,11 @@ describe("RustCrypto", () => {
|
||||
const inputs: IToDeviceEvent[] = [
|
||||
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
|
||||
];
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
const res = (await rustCrypto.preprocessToDeviceMessages(inputs)).map((p) => p.message);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
|
||||
it("should pass through bad encrypted messages", async () => {
|
||||
it("should fail to process bad encrypted messages", async () => {
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
const keys = olmMachine.identityKeys;
|
||||
const inputs: IToDeviceEvent[] = [
|
||||
@ -576,7 +576,7 @@ describe("RustCrypto", () => {
|
||||
];
|
||||
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
expect(res.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
||||
|
@ -87,7 +87,12 @@ import { type IIdentityServerProvider } from "./@types/IIdentityServerProvider.t
|
||||
import { type MatrixScheduler } from "./scheduler.ts";
|
||||
import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts";
|
||||
import { type AuthDict } from "./interactive-auth.ts";
|
||||
import { type IMinimalEvent, type IRoomEvent, type IStateEvent } from "./sync-accumulator.ts";
|
||||
import {
|
||||
type IMinimalEvent,
|
||||
type IRoomEvent,
|
||||
type IStateEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "./sync-accumulator.ts";
|
||||
import type { EventTimelineSet } from "./models/event-timeline-set.ts";
|
||||
import * as ContentHelpers from "./content-helpers.ts";
|
||||
import {
|
||||
@ -885,7 +890,9 @@ const EVENT_ID_PREFIX = "$";
|
||||
export enum ClientEvent {
|
||||
Sync = "sync",
|
||||
Event = "event",
|
||||
/** @deprecated Use {@link ReceivedToDeviceMessage}. */
|
||||
ToDeviceEvent = "toDeviceEvent",
|
||||
ReceivedToDeviceMessage = "receivedToDeviceMessage",
|
||||
AccountData = "accountData",
|
||||
Room = "Room",
|
||||
DeleteRoom = "deleteRoom",
|
||||
@ -1088,6 +1095,20 @@ export type ClientEventHandlerMap = {
|
||||
* ```
|
||||
*/
|
||||
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
/**
|
||||
* Fires whenever the SDK receives a new to-device message.
|
||||
* @param payload - The message and encryptionInfo for this message (See {@link ReceivedToDeviceMessage}) which caused this event to fire.
|
||||
* @example
|
||||
* ```
|
||||
* matrixClient.on("receivedToDeviceMessage", function(payload){
|
||||
* const { message, encryptionInfo } = payload;
|
||||
* var claimed_sender = encryptionInfo ? encryptionInfo.sender : message.sender;
|
||||
* var isVerified = encryptionInfo ? encryptionInfo.verified : false;
|
||||
* var type = message.type;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
[ClientEvent.ReceivedToDeviceMessage]: (payload: ReceivedToDeviceMessage) => void;
|
||||
/**
|
||||
* Fires if a to-device event is received that cannot be decrypted.
|
||||
* Encrypted to-device events will (generally) use plain Olm encryption,
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
|
||||
import type { IDeviceLists, IToDeviceEvent, ReceivedToDeviceMessage } from "../sync-accumulator.ts";
|
||||
import { type IClearEvent, type MatrixEvent } from "../models/event.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.ts";
|
||||
@ -96,9 +96,11 @@ export interface SyncCryptoCallbacks {
|
||||
* messages, rather than the results of any decryption attempts.
|
||||
*
|
||||
* @param events - the received to-device messages
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
* @returns A list of preprocessed to-device messages. This will not map 1:1 to the input list, as some messages may be invalid or
|
||||
* failed to decrypt, and so will be omitted from the output list.
|
||||
*
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop when one time key counts and unused fallback key details are received.
|
||||
|
@ -1365,6 +1365,27 @@ export interface OwnDeviceKeys {
|
||||
curve25519: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the encryption of a successfully decrypted to-device message.
|
||||
*/
|
||||
export interface OlmEncryptionInfo {
|
||||
/** The user ID of the event sender, note this is untrusted data unless `isVerified` is true **/
|
||||
sender: string;
|
||||
/**
|
||||
* The device ID of the device that sent us the event.
|
||||
* Note this is untrusted data unless {@link senderVerified} is true.
|
||||
* If the device ID is not known, this will be `null`.
|
||||
**/
|
||||
senderDevice?: string;
|
||||
/** The sender device's public Curve25519 key, base64 encoded **/
|
||||
senderCurve25519KeyBase64: string;
|
||||
/**
|
||||
* If true, this message is guaranteed to be authentic as it is coming from a device belonging to a user that we have verified.
|
||||
* This is the state at the time of decryption (the user could be verified later).
|
||||
*/
|
||||
senderVerified: boolean;
|
||||
}
|
||||
|
||||
export * from "./verification.ts";
|
||||
export type * from "./keybackup.ts";
|
||||
export * from "./recovery-key.ts";
|
||||
|
@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
|
||||
import { type IDeviceLists, type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
|
||||
import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
|
||||
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
@ -1481,7 +1481,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* @param oneTimeKeysCounts - the received one time key counts
|
||||
* @param unusedFallbackKeys - the received unused fallback keys
|
||||
* @param devices - the received device list updates
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
* @returns A list of processed to-device messages.
|
||||
*/
|
||||
private async receiveSyncChanges({
|
||||
events,
|
||||
@ -1493,15 +1493,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
oneTimeKeysCounts?: Map<string, number>;
|
||||
unusedFallbackKeys?: Set<string>;
|
||||
devices?: RustSdkCryptoJs.DeviceLists;
|
||||
}): Promise<IToDeviceEvent[]> {
|
||||
const result = await this.olmMachine.receiveSyncChanges(
|
||||
}): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
|
||||
return await this.olmMachine.receiveSyncChanges(
|
||||
events ? JSON.stringify(events) : "[]",
|
||||
devices,
|
||||
oneTimeKeysCounts,
|
||||
unusedFallbackKeys,
|
||||
);
|
||||
|
||||
return result.map((processed) => JSON.parse(processed.rawEvent));
|
||||
}
|
||||
|
||||
/** called by the sync loop to preprocess incoming to-device messages
|
||||
@ -1509,22 +1507,56 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* @param events - the received to-device messages
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]> {
|
||||
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
||||
// one-time-keys, or fallback keys, so just pass empty data.
|
||||
const processed = await this.receiveSyncChanges({ events });
|
||||
|
||||
// look for interesting to-device messages
|
||||
const received: ReceivedToDeviceMessage[] = [];
|
||||
|
||||
for (const message of processed) {
|
||||
if (message.type === EventType.KeyVerificationRequest) {
|
||||
const sender = message.sender;
|
||||
const transactionId = message.content.transaction_id;
|
||||
const parsedMessage: IToDeviceEvent = JSON.parse(message.rawEvent);
|
||||
|
||||
// look for interesting to-device messages
|
||||
if (parsedMessage.type === EventType.KeyVerificationRequest) {
|
||||
const sender = parsedMessage.sender;
|
||||
const transactionId = parsedMessage.content.transaction_id;
|
||||
if (transactionId && sender) {
|
||||
this.onIncomingKeyVerificationRequest(sender, transactionId);
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case RustSdkCryptoJs.ProcessedToDeviceEventType.Decrypted: {
|
||||
const encryptionInfo = (message as RustSdkCryptoJs.DecryptedToDeviceEvent).encryptionInfo;
|
||||
received.push({
|
||||
message: parsedMessage,
|
||||
encryptionInfo: {
|
||||
sender: encryptionInfo.sender.toString(),
|
||||
senderDevice: encryptionInfo.senderDevice?.toString(),
|
||||
senderCurve25519KeyBase64: encryptionInfo.senderCurve25519Key,
|
||||
senderVerified: encryptionInfo.isSenderVerified(),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case RustSdkCryptoJs.ProcessedToDeviceEventType.PlainText: {
|
||||
received.push({
|
||||
message: parsedMessage,
|
||||
encryptionInfo: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case RustSdkCryptoJs.ProcessedToDeviceEventType.UnableToDecrypt:
|
||||
// ignore messages we cannot decrypt
|
||||
break;
|
||||
case RustSdkCryptoJs.ProcessedToDeviceEventType.Invalid:
|
||||
// ignore invalid messages
|
||||
break;
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
|
||||
return received;
|
||||
}
|
||||
|
||||
/** called by the sync loop to process one time key counts and unused fallback keys
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
type IStateEvent,
|
||||
type IStrippedState,
|
||||
type ISyncResponse,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "./sync-accumulator.ts";
|
||||
import { MatrixError } from "./http-api/index.ts";
|
||||
import {
|
||||
@ -150,11 +151,20 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
|
||||
}
|
||||
|
||||
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
||||
let events = data["events"] || [];
|
||||
if (events.length > 0 && this.cryptoCallbacks) {
|
||||
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
||||
const events = data["events"] || [];
|
||||
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
|
||||
if (this.cryptoCallbacks) {
|
||||
receivedToDeviceMessages = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
||||
} else {
|
||||
receivedToDeviceMessages = events.map((rawEvent) =>
|
||||
// Crypto is not enabled, so we just return the events.
|
||||
({
|
||||
message: rawEvent,
|
||||
encryptionInfo: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
processToDeviceMessages(events, this.client);
|
||||
processToDeviceMessages(receivedToDeviceMessages, this.client);
|
||||
|
||||
this.nextBatch = data.next_batch;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import { type IRoomSummary } from "./models/room-summary.ts";
|
||||
import { type EventType } from "./@types/event.ts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
|
||||
import { ReceiptAccumulator } from "./receipt-accumulator.ts";
|
||||
import { type OlmEncryptionInfo } from "./crypto-api/index.ts";
|
||||
|
||||
interface IOpts {
|
||||
/**
|
||||
@ -134,12 +135,31 @@ interface IAccountData {
|
||||
events: IMinimalEvent[];
|
||||
}
|
||||
|
||||
/** A to-device message as received from the sync. */
|
||||
export interface IToDeviceEvent {
|
||||
content: IContent;
|
||||
sender: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A (possibly decrypted) to-device message after it has been successfully processed by the sdk.
|
||||
*
|
||||
* If the message was encrypted, the `encryptionInfo` field will contain the encryption information.
|
||||
* If the message was sent in clear, this field will be null.
|
||||
*
|
||||
* The `message` field contains the message `type`, `content`, and `sender` as if the message was sent in clear.
|
||||
*/
|
||||
export interface ReceivedToDeviceMessage {
|
||||
/** The message type, content, and sender as if the message was sent in clear. */
|
||||
message: IToDeviceEvent;
|
||||
/**
|
||||
* Information about the encryption of the message.
|
||||
* Will be null if the message was sent in clear
|
||||
*/
|
||||
encryptionInfo: OlmEncryptionInfo | null;
|
||||
}
|
||||
|
||||
interface IToDevice {
|
||||
events: IToDeviceEvent[];
|
||||
}
|
||||
|
89
src/sync.ts
89
src/sync.ts
@ -44,8 +44,8 @@ import {
|
||||
type IInvitedRoom,
|
||||
type IInviteState,
|
||||
type IJoinedRoom,
|
||||
type ILeftRoom,
|
||||
type IKnockedRoom,
|
||||
type ILeftRoom,
|
||||
type IMinimalEvent,
|
||||
type IRoomEvent,
|
||||
type IStateEvent,
|
||||
@ -53,13 +53,14 @@ import {
|
||||
type ISyncResponse,
|
||||
type ITimeline,
|
||||
type IToDeviceEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "./sync-accumulator.ts";
|
||||
import { MatrixEvent, type IEvent } from "./models/event.ts";
|
||||
import { MatrixEvent } from "./models/event.ts";
|
||||
import { type MatrixError, Method } from "./http-api/index.ts";
|
||||
import { type ISavedSync } from "./store/index.ts";
|
||||
import { EventType } from "./@types/event.ts";
|
||||
import { type IPushRules } from "./@types/PushRules.ts";
|
||||
import { RoomStateEvent, type IMarkerFoundOptions } from "./models/room-state.ts";
|
||||
import { type IMarkerFoundOptions, RoomStateEvent } from "./models/room-state.ts";
|
||||
import { RoomMemberEvent } from "./models/room-member.ts";
|
||||
import { BeaconEvent } from "./models/beacon.ts";
|
||||
import { type IEventsResponse } from "./@types/requests.ts";
|
||||
@ -1144,13 +1145,23 @@ export class SyncApi {
|
||||
|
||||
// handle to-device events
|
||||
if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
|
||||
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
|
||||
const toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
|
||||
|
||||
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
|
||||
receivedToDeviceMessages =
|
||||
await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
|
||||
} else {
|
||||
receivedToDeviceMessages = toDeviceMessages.map((rawEvent) =>
|
||||
// Crypto is not enabled, so we just return the events.
|
||||
({
|
||||
message: rawEvent,
|
||||
encryptionInfo: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
processToDeviceMessages(toDeviceMessages, client);
|
||||
processToDeviceMessages(receivedToDeviceMessages, client);
|
||||
} else {
|
||||
// no more to-device events: we can stop polling with a short timeout.
|
||||
this.catchingUp = false;
|
||||
@ -1909,21 +1920,21 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
|
||||
/**
|
||||
* Process a list of (decrypted, where possible) received to-device events.
|
||||
*
|
||||
* Converts the events into `MatrixEvent`s, and emits appropriate {@link ClientEvent.ToDeviceEvent} events.
|
||||
* Emits the appropriate {@link ClientEvent.ReceivedToDeviceMessage} event.
|
||||
* Also converts the events into `MatrixEvent`s, and emits the now deprecated {@link ClientEvent.ToDeviceEvent} events for compatibility.
|
||||
* */
|
||||
export function processToDeviceMessages(toDeviceMessages: IToDeviceEvent[], client: MatrixClient): void {
|
||||
export function processToDeviceMessages(toDeviceMessages: ReceivedToDeviceMessage[], client: MatrixClient): void {
|
||||
const cancelledKeyVerificationTxns: string[] = [];
|
||||
toDeviceMessages
|
||||
.map(mapToDeviceEvent)
|
||||
.map((toDeviceEvent) => {
|
||||
.map((processedMessage) => {
|
||||
// map is a cheap inline forEach
|
||||
// We want to flag m.key.verification.start events as cancelled
|
||||
// if there's an accompanying m.key.verification.cancel event, so
|
||||
// we pull out the transaction IDs from the cancellation events
|
||||
// so we can flag the verification events as cancelled in the loop
|
||||
// below.
|
||||
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
|
||||
const txnId: string = toDeviceEvent.getContent()["transaction_id"];
|
||||
if (processedMessage.message.type === "m.key.verification.cancel") {
|
||||
const txnId: string = processedMessage.message.content["transaction_id"];
|
||||
if (txnId) {
|
||||
cancelledKeyVerificationTxns.push(txnId);
|
||||
}
|
||||
@ -1931,32 +1942,40 @@ export function processToDeviceMessages(toDeviceMessages: IToDeviceEvent[], clie
|
||||
|
||||
// as mentioned above, .map is a cheap inline forEach, so return
|
||||
// the unmodified event.
|
||||
return toDeviceEvent;
|
||||
return processedMessage;
|
||||
})
|
||||
.forEach(function (toDeviceEvent) {
|
||||
const content = toDeviceEvent.getContent();
|
||||
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
|
||||
// the mapper already logged a warning.
|
||||
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
toDeviceEvent.getType() === "m.key.verification.start" ||
|
||||
toDeviceEvent.getType() === "m.key.verification.request"
|
||||
) {
|
||||
const txnId = content["transaction_id"];
|
||||
if (cancelledKeyVerificationTxns.includes(txnId)) {
|
||||
toDeviceEvent.flagCancelled();
|
||||
.forEach(function (processedEvent) {
|
||||
// For backwards compatibility, we also emit the event as a `MatrixEvent` using `ClientEvent.ToDeviceEvent`.
|
||||
{
|
||||
const toDeviceEvent = processedEvent.message;
|
||||
const content = toDeviceEvent.content;
|
||||
// The message is cloned before being passed to the MatrixEvent constructor, because
|
||||
// the `makeEncrypted` method will mutate the type and content properties of the original message and will interfere
|
||||
// with the emitted event for `ReceivedToDeviceMessage`.
|
||||
const deprecatedCompatibilityEvent = new MatrixEvent(Object.assign({}, toDeviceEvent));
|
||||
if (
|
||||
toDeviceEvent.type === "m.key.verification.start" ||
|
||||
toDeviceEvent.type === "m.key.verification.request"
|
||||
) {
|
||||
const txnId = content["transaction_id"];
|
||||
if (cancelledKeyVerificationTxns.includes(txnId)) {
|
||||
deprecatedCompatibilityEvent.flagCancelled();
|
||||
}
|
||||
}
|
||||
if (processedEvent.encryptionInfo) {
|
||||
// Restore partially the legacy behavior to detect encrypted messages.
|
||||
// Now `event.isEncrypted()` will return true.
|
||||
deprecatedCompatibilityEvent.makeEncrypted(
|
||||
EventType.RoomMessageEncrypted,
|
||||
{ ciphertext: "" },
|
||||
processedEvent.encryptionInfo.senderCurve25519KeyBase64,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
client.emit(ClientEvent.ToDeviceEvent, deprecatedCompatibilityEvent);
|
||||
}
|
||||
|
||||
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
|
||||
client.emit(ClientEvent.ReceivedToDeviceMessage, processedEvent);
|
||||
});
|
||||
}
|
||||
|
||||
function mapToDeviceEvent(plainOldJsObject: Partial<IEvent>): MatrixEvent {
|
||||
// to-device events should not have a `room_id` property, but let's be sure
|
||||
delete plainOldJsObject.room_id;
|
||||
return new MatrixEvent(plainOldJsObject);
|
||||
}
|
||||
|
Reference in New Issue
Block a user