You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Pass to-device messages into rust crypto-sdk (#3021)
We need a separate API, because `ClientEvent.ToDeviceEvent` is only emitted for successfully decrypted to-device events
This commit is contained in:
committed by
GitHub
parent
22f10f71b8
commit
030abe1563
@@ -22,6 +22,7 @@ import {
|
|||||||
KeysClaimRequest,
|
KeysClaimRequest,
|
||||||
KeysQueryRequest,
|
KeysQueryRequest,
|
||||||
KeysUploadRequest,
|
KeysUploadRequest,
|
||||||
|
OlmMachine,
|
||||||
SignatureUploadRequest,
|
SignatureUploadRequest,
|
||||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
import { Mocked } from "jest-mock";
|
import { Mocked } from "jest-mock";
|
||||||
@@ -29,7 +30,7 @@ import MockHttpBackend from "matrix-mock-request";
|
|||||||
|
|
||||||
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
||||||
import { initRustCrypto } from "../../src/rust-crypto";
|
import { initRustCrypto } from "../../src/rust-crypto";
|
||||||
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi } from "../../src";
|
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IToDeviceEvent, MatrixHttpApi } from "../../src";
|
||||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -57,6 +58,47 @@ describe("RustCrypto", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("to-device messages", () => {
|
||||||
|
let rustCrypto: RustCrypto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockHttpApi = {} as MatrixHttpApi<IHttpOpts>;
|
||||||
|
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through unencrypted to-device messages", async () => {
|
||||||
|
const inputs: IToDeviceEvent[] = [
|
||||||
|
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
|
||||||
|
];
|
||||||
|
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||||
|
expect(res).toEqual(inputs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through bad encrypted messages", async () => {
|
||||||
|
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||||
|
const keys = olmMachine.identityKeys;
|
||||||
|
const inputs: IToDeviceEvent[] = [
|
||||||
|
{
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
|
||||||
|
ciphertext: {
|
||||||
|
[keys.curve25519.toBase64()]: {
|
||||||
|
type: 0,
|
||||||
|
body: "ajyjlghi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||||
|
expect(res).toEqual(inputs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("outgoing requests", () => {
|
describe("outgoing requests", () => {
|
||||||
/** the RustCrypto implementation under test */
|
/** the RustCrypto implementation under test */
|
||||||
let rustCrypto: RustCrypto;
|
let rustCrypto: RustCrypto;
|
||||||
|
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
|
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,6 +75,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
|||||||
|
|
||||||
/** The methods which crypto implementations should expose to the Sync api */
|
/** The methods which crypto implementations should expose to the Sync api */
|
||||||
export interface SyncCryptoCallbacks {
|
export interface SyncCryptoCallbacks {
|
||||||
|
/**
|
||||||
|
* Called by the /sync loop whenever there are incoming to-device messages.
|
||||||
|
*
|
||||||
|
* The implementation may preprocess the received messages (eg, decrypt them) and return an
|
||||||
|
* updated list of messages for dispatch to the rest of the system.
|
||||||
|
*
|
||||||
|
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
|
||||||
|
* messages, rather than the results of any decryption attempts.
|
||||||
|
*
|
||||||
|
* @param events - the received to-device messages
|
||||||
|
* @returns A list of preprocessed to-device messages.
|
||||||
|
*/
|
||||||
|
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the /sync loop after each /sync response is processed.
|
* Called by the /sync loop after each /sync response is processed.
|
||||||
*
|
*
|
||||||
|
@@ -85,7 +85,7 @@ import { CryptoStore } from "./store/base";
|
|||||||
import { IVerificationChannel } from "./verification/request/Channel";
|
import { IVerificationChannel } from "./verification/request/Channel";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||||
import { IContent } from "../models/event";
|
import { IContent } from "../models/event";
|
||||||
import { ISyncResponse } from "../sync-accumulator";
|
import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISignatures } from "../@types/signed";
|
||||||
import { IMessage } from "./algorithms/olm";
|
import { IMessage } from "./algorithms/olm";
|
||||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||||
@@ -3198,6 +3198,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||||
|
// all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
|
||||||
|
// happens later in decryptEvent, via the EventMapper
|
||||||
|
return events.filter((toDevice) => {
|
||||||
|
if (
|
||||||
|
toDevice.type === EventType.RoomMessageEncrypted &&
|
||||||
|
!["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
|
||||||
|
) {
|
||||||
|
logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||||
try {
|
try {
|
||||||
logger.log(
|
logger.log(
|
||||||
|
@@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
event.setThread(thread);
|
event.setThread(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
|
||||||
|
// to-device events), because the rust implementation decrypts to-device messages at a higher level.
|
||||||
|
// Generally we probably want to use a different eventMapper implementation for to-device events because
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
|
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
|
||||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
|
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
@@ -93,6 +94,25 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
//
|
//
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/** called by the sync loop to preprocess incoming to-device messages
|
||||||
|
*
|
||||||
|
* @param events - the received to-device messages
|
||||||
|
* @returns A list of preprocessed to-device messages.
|
||||||
|
*/
|
||||||
|
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||||
|
// 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 result = await this.olmMachine.receiveSyncChanges(
|
||||||
|
JSON.stringify(events),
|
||||||
|
new RustSdkCryptoJs.DeviceLists(),
|
||||||
|
new Map(),
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
/** called by the sync loop after processing each sync.
|
/** called by the sync loop after processing each sync.
|
||||||
*
|
*
|
||||||
* TODO: figure out something equivalent for sliding sync.
|
* TODO: figure out something equivalent for sliding sync.
|
||||||
|
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
@@ -127,7 +128,7 @@ type ExtensionToDeviceResponse = {
|
|||||||
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
|
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
|
||||||
private nextBatch: string | null = null;
|
private nextBatch: string | null = null;
|
||||||
|
|
||||||
public constructor(private readonly client: MatrixClient) {}
|
public constructor(private readonly client: MatrixClient, private readonly cryptoCallbacks?: SyncCryptoCallbacks) {}
|
||||||
|
|
||||||
public name(): string {
|
public name(): string {
|
||||||
return "to_device";
|
return "to_device";
|
||||||
@@ -150,8 +151,12 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
|
|||||||
|
|
||||||
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
||||||
const cancelledKeyVerificationTxns: string[] = [];
|
const cancelledKeyVerificationTxns: string[] = [];
|
||||||
data.events
|
let events = data["events"] || [];
|
||||||
?.map(this.client.getEventMapper())
|
if (events.length > 0 && this.cryptoCallbacks) {
|
||||||
|
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
||||||
|
}
|
||||||
|
events
|
||||||
|
.map(this.client.getEventMapper())
|
||||||
.map((toDeviceEvent) => {
|
.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
|
||||||
@@ -373,7 +378,7 @@ export class SlidingSyncSdk {
|
|||||||
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
|
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
|
||||||
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
|
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
|
||||||
const extensions: Extension<any, any>[] = [
|
const extensions: Extension<any, any>[] = [
|
||||||
new ExtensionToDevice(this.client),
|
new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks),
|
||||||
new ExtensionAccountData(this.client),
|
new ExtensionAccountData(this.client),
|
||||||
new ExtensionTyping(this.client),
|
new ExtensionTyping(this.client),
|
||||||
new ExtensionReceipts(this.client),
|
new ExtensionReceipts(this.client),
|
||||||
|
21
src/sync.ts
21
src/sync.ts
@@ -48,6 +48,7 @@ import {
|
|||||||
IStrippedState,
|
IStrippedState,
|
||||||
ISyncResponse,
|
ISyncResponse,
|
||||||
ITimeline,
|
ITimeline,
|
||||||
|
IToDeviceEvent,
|
||||||
} from "./sync-accumulator";
|
} from "./sync-accumulator";
|
||||||
import { MatrixEvent } from "./models/event";
|
import { MatrixEvent } from "./models/event";
|
||||||
import { MatrixError, Method } from "./http-api";
|
import { MatrixError, Method } from "./http-api";
|
||||||
@@ -1170,19 +1171,15 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle to-device events
|
// handle to-device events
|
||||||
if (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) {
|
||||||
const cancelledKeyVerificationTxns: string[] = [];
|
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events;
|
||||||
data.to_device!.events.filter((eventJSON) => {
|
|
||||||
if (
|
|
||||||
eventJSON.type === EventType.RoomMessageEncrypted &&
|
|
||||||
!["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm)
|
|
||||||
) {
|
|
||||||
logger.log("Ignoring invalid encrypted to-device event from " + eventJSON.sender);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
if (this.syncOpts.cryptoCallbacks) {
|
||||||
})
|
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelledKeyVerificationTxns: string[] = [];
|
||||||
|
toDeviceMessages
|
||||||
.map(client.getEventMapper({ toDevice: true }))
|
.map(client.getEventMapper({ toDevice: true }))
|
||||||
.map((toDeviceEvent) => {
|
.map((toDeviceEvent) => {
|
||||||
// map is a cheap inline forEach
|
// map is a cheap inline forEach
|
||||||
|
Reference in New Issue
Block a user