You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +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 fetchMock from "fetch-mock-jest";
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
|
import Olm from "@matrix-org/olm";
|
||||||
|
|
||||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
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 * as testData from "../../test-utils/test-data";
|
||||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
|
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// 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 */
|
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||||
let e2eKeyResponder: E2EKeyResponder;
|
let e2eKeyResponder: E2EKeyResponder;
|
||||||
|
let e2eKeyReceiver: E2EKeyReceiver;
|
||||||
|
let syncResponder: SyncResponder;
|
||||||
|
|
||||||
beforeEach(
|
beforeEach(
|
||||||
async () => {
|
async () => {
|
||||||
@ -59,8 +70,8 @@ describe("to-device-messages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
new E2EKeyReceiver(homeserverUrl);
|
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||||
const syncResponder = new SyncResponder(homeserverUrl);
|
syncResponder = new SyncResponder(homeserverUrl);
|
||||||
|
|
||||||
// add bob as known user
|
// add bob as known user
|
||||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
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?
|
// 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[] = [
|
const inputs: IToDeviceEvent[] = [
|
||||||
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
|
{ 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);
|
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 olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||||
const keys = olmMachine.identityKeys;
|
const keys = olmMachine.identityKeys;
|
||||||
const inputs: IToDeviceEvent[] = [
|
const inputs: IToDeviceEvent[] = [
|
||||||
@ -576,7 +576,7 @@ describe("RustCrypto", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
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 () => {
|
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 MatrixScheduler } from "./scheduler.ts";
|
||||||
import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts";
|
import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts";
|
||||||
import { type AuthDict } from "./interactive-auth.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 type { EventTimelineSet } from "./models/event-timeline-set.ts";
|
||||||
import * as ContentHelpers from "./content-helpers.ts";
|
import * as ContentHelpers from "./content-helpers.ts";
|
||||||
import {
|
import {
|
||||||
@ -885,7 +890,9 @@ const EVENT_ID_PREFIX = "$";
|
|||||||
export enum ClientEvent {
|
export enum ClientEvent {
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
Event = "event",
|
Event = "event",
|
||||||
|
/** @deprecated Use {@link ReceivedToDeviceMessage}. */
|
||||||
ToDeviceEvent = "toDeviceEvent",
|
ToDeviceEvent = "toDeviceEvent",
|
||||||
|
ReceivedToDeviceMessage = "receivedToDeviceMessage",
|
||||||
AccountData = "accountData",
|
AccountData = "accountData",
|
||||||
Room = "Room",
|
Room = "Room",
|
||||||
DeleteRoom = "deleteRoom",
|
DeleteRoom = "deleteRoom",
|
||||||
@ -1088,6 +1095,20 @@ export type ClientEventHandlerMap = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
|
[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.
|
* Fires if a to-device event is received that cannot be decrypted.
|
||||||
* Encrypted to-device events will (generally) use plain Olm encryption,
|
* 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.
|
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 IClearEvent, type MatrixEvent } from "../models/event.ts";
|
||||||
import { type Room } from "../models/room.ts";
|
import { type Room } from "../models/room.ts";
|
||||||
import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.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.
|
* messages, rather than the results of any decryption attempts.
|
||||||
*
|
*
|
||||||
* @param events - the received to-device messages
|
* @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.
|
* 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;
|
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 * from "./verification.ts";
|
||||||
export type * from "./keybackup.ts";
|
export type * from "./keybackup.ts";
|
||||||
export * from "./recovery-key.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 type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto.ts";
|
||||||
import { KnownMembership } from "../@types/membership.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 { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
|
||||||
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
|
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
|
||||||
import { type Room } from "../models/room.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 oneTimeKeysCounts - the received one time key counts
|
||||||
* @param unusedFallbackKeys - the received unused fallback keys
|
* @param unusedFallbackKeys - the received unused fallback keys
|
||||||
* @param devices - the received device list updates
|
* @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({
|
private async receiveSyncChanges({
|
||||||
events,
|
events,
|
||||||
@ -1493,15 +1493,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
oneTimeKeysCounts?: Map<string, number>;
|
oneTimeKeysCounts?: Map<string, number>;
|
||||||
unusedFallbackKeys?: Set<string>;
|
unusedFallbackKeys?: Set<string>;
|
||||||
devices?: RustSdkCryptoJs.DeviceLists;
|
devices?: RustSdkCryptoJs.DeviceLists;
|
||||||
}): Promise<IToDeviceEvent[]> {
|
}): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
|
||||||
const result = await this.olmMachine.receiveSyncChanges(
|
return await this.olmMachine.receiveSyncChanges(
|
||||||
events ? JSON.stringify(events) : "[]",
|
events ? JSON.stringify(events) : "[]",
|
||||||
devices,
|
devices,
|
||||||
oneTimeKeysCounts,
|
oneTimeKeysCounts,
|
||||||
unusedFallbackKeys,
|
unusedFallbackKeys,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.map((processed) => JSON.parse(processed.rawEvent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** called by the sync loop to preprocess incoming to-device messages
|
/** 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
|
* @param events - the received to-device messages
|
||||||
* @returns A list of preprocessed 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,
|
// 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.
|
// one-time-keys, or fallback keys, so just pass empty data.
|
||||||
const processed = await this.receiveSyncChanges({ events });
|
const processed = await this.receiveSyncChanges({ events });
|
||||||
|
|
||||||
// look for interesting to-device messages
|
const received: ReceivedToDeviceMessage[] = [];
|
||||||
|
|
||||||
for (const message of processed) {
|
for (const message of processed) {
|
||||||
if (message.type === EventType.KeyVerificationRequest) {
|
const parsedMessage: IToDeviceEvent = JSON.parse(message.rawEvent);
|
||||||
const sender = message.sender;
|
|
||||||
const transactionId = message.content.transaction_id;
|
// look for interesting to-device messages
|
||||||
|
if (parsedMessage.type === EventType.KeyVerificationRequest) {
|
||||||
|
const sender = parsedMessage.sender;
|
||||||
|
const transactionId = parsedMessage.content.transaction_id;
|
||||||
if (transactionId && sender) {
|
if (transactionId && sender) {
|
||||||
this.onIncomingKeyVerificationRequest(sender, transactionId);
|
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
|
/** called by the sync loop to process one time key counts and unused fallback keys
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
type IStateEvent,
|
type IStateEvent,
|
||||||
type IStrippedState,
|
type IStrippedState,
|
||||||
type ISyncResponse,
|
type ISyncResponse,
|
||||||
|
type ReceivedToDeviceMessage,
|
||||||
} from "./sync-accumulator.ts";
|
} from "./sync-accumulator.ts";
|
||||||
import { MatrixError } from "./http-api/index.ts";
|
import { MatrixError } from "./http-api/index.ts";
|
||||||
import {
|
import {
|
||||||
@ -150,11 +151,20 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
||||||
let events = data["events"] || [];
|
const events = data["events"] || [];
|
||||||
if (events.length > 0 && this.cryptoCallbacks) {
|
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
|
||||||
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
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;
|
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 { type EventType } from "./@types/event.ts";
|
||||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
|
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
|
||||||
import { ReceiptAccumulator } from "./receipt-accumulator.ts";
|
import { ReceiptAccumulator } from "./receipt-accumulator.ts";
|
||||||
|
import { type OlmEncryptionInfo } from "./crypto-api/index.ts";
|
||||||
|
|
||||||
interface IOpts {
|
interface IOpts {
|
||||||
/**
|
/**
|
||||||
@ -134,12 +135,31 @@ interface IAccountData {
|
|||||||
events: IMinimalEvent[];
|
events: IMinimalEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A to-device message as received from the sync. */
|
||||||
export interface IToDeviceEvent {
|
export interface IToDeviceEvent {
|
||||||
content: IContent;
|
content: IContent;
|
||||||
sender: string;
|
sender: string;
|
||||||
type: 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 {
|
interface IToDevice {
|
||||||
events: IToDeviceEvent[];
|
events: IToDeviceEvent[];
|
||||||
}
|
}
|
||||||
|
89
src/sync.ts
89
src/sync.ts
@ -44,8 +44,8 @@ import {
|
|||||||
type IInvitedRoom,
|
type IInvitedRoom,
|
||||||
type IInviteState,
|
type IInviteState,
|
||||||
type IJoinedRoom,
|
type IJoinedRoom,
|
||||||
type ILeftRoom,
|
|
||||||
type IKnockedRoom,
|
type IKnockedRoom,
|
||||||
|
type ILeftRoom,
|
||||||
type IMinimalEvent,
|
type IMinimalEvent,
|
||||||
type IRoomEvent,
|
type IRoomEvent,
|
||||||
type IStateEvent,
|
type IStateEvent,
|
||||||
@ -53,13 +53,14 @@ import {
|
|||||||
type ISyncResponse,
|
type ISyncResponse,
|
||||||
type ITimeline,
|
type ITimeline,
|
||||||
type IToDeviceEvent,
|
type IToDeviceEvent,
|
||||||
|
type ReceivedToDeviceMessage,
|
||||||
} from "./sync-accumulator.ts";
|
} 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 MatrixError, Method } from "./http-api/index.ts";
|
||||||
import { type ISavedSync } from "./store/index.ts";
|
import { type ISavedSync } from "./store/index.ts";
|
||||||
import { EventType } from "./@types/event.ts";
|
import { EventType } from "./@types/event.ts";
|
||||||
import { type IPushRules } from "./@types/PushRules.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 { RoomMemberEvent } from "./models/room-member.ts";
|
||||||
import { BeaconEvent } from "./models/beacon.ts";
|
import { BeaconEvent } from "./models/beacon.ts";
|
||||||
import { type IEventsResponse } from "./@types/requests.ts";
|
import { type IEventsResponse } from "./@types/requests.ts";
|
||||||
@ -1144,13 +1145,23 @@ export class SyncApi {
|
|||||||
|
|
||||||
// handle to-device events
|
// handle to-device events
|
||||||
if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
|
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) {
|
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 {
|
} else {
|
||||||
// no more to-device events: we can stop polling with a short timeout.
|
// no more to-device events: we can stop polling with a short timeout.
|
||||||
this.catchingUp = false;
|
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.
|
* 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[] = [];
|
const cancelledKeyVerificationTxns: string[] = [];
|
||||||
toDeviceMessages
|
toDeviceMessages
|
||||||
.map(mapToDeviceEvent)
|
.map((processedMessage) => {
|
||||||
.map((toDeviceEvent) => {
|
|
||||||
// map is a cheap inline forEach
|
// map is a cheap inline forEach
|
||||||
// We want to flag m.key.verification.start events as cancelled
|
// We want to flag m.key.verification.start events as cancelled
|
||||||
// if there's an accompanying m.key.verification.cancel event, so
|
// if there's an accompanying m.key.verification.cancel event, so
|
||||||
// we pull out the transaction IDs from the cancellation events
|
// we pull out the transaction IDs from the cancellation events
|
||||||
// so we can flag the verification events as cancelled in the loop
|
// so we can flag the verification events as cancelled in the loop
|
||||||
// below.
|
// below.
|
||||||
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
|
if (processedMessage.message.type === "m.key.verification.cancel") {
|
||||||
const txnId: string = toDeviceEvent.getContent()["transaction_id"];
|
const txnId: string = processedMessage.message.content["transaction_id"];
|
||||||
if (txnId) {
|
if (txnId) {
|
||||||
cancelledKeyVerificationTxns.push(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
|
// as mentioned above, .map is a cheap inline forEach, so return
|
||||||
// the unmodified event.
|
// the unmodified event.
|
||||||
return toDeviceEvent;
|
return processedMessage;
|
||||||
})
|
})
|
||||||
.forEach(function (toDeviceEvent) {
|
.forEach(function (processedEvent) {
|
||||||
const content = toDeviceEvent.getContent();
|
// For backwards compatibility, we also emit the event as a `MatrixEvent` using `ClientEvent.ToDeviceEvent`.
|
||||||
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
|
{
|
||||||
// the mapper already logged a warning.
|
const toDeviceEvent = processedEvent.message;
|
||||||
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
|
const content = toDeviceEvent.content;
|
||||||
return;
|
// 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`.
|
||||||
if (
|
const deprecatedCompatibilityEvent = new MatrixEvent(Object.assign({}, toDeviceEvent));
|
||||||
toDeviceEvent.getType() === "m.key.verification.start" ||
|
if (
|
||||||
toDeviceEvent.getType() === "m.key.verification.request"
|
toDeviceEvent.type === "m.key.verification.start" ||
|
||||||
) {
|
toDeviceEvent.type === "m.key.verification.request"
|
||||||
const txnId = content["transaction_id"];
|
) {
|
||||||
if (cancelledKeyVerificationTxns.includes(txnId)) {
|
const txnId = content["transaction_id"];
|
||||||
toDeviceEvent.flagCancelled();
|
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