Skip to content

Commit 030abe1

Browse files
authored
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
1 parent 22f10f7 commit 030abe1

File tree

7 files changed

+115
-18
lines changed

7 files changed

+115
-18
lines changed

spec/unit/rust-crypto.spec.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import {
2222
KeysClaimRequest,
2323
KeysQueryRequest,
2424
KeysUploadRequest,
25+
OlmMachine,
2526
SignatureUploadRequest,
2627
} from "@matrix-org/matrix-sdk-crypto-js";
2728
import { Mocked } from "jest-mock";
2829
import MockHttpBackend from "matrix-mock-request";
2930

3031
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
3132
import { initRustCrypto } from "../../src/rust-crypto";
32-
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi } from "../../src";
33+
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IToDeviceEvent, MatrixHttpApi } from "../../src";
3334
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
3435

3536
afterEach(() => {
@@ -57,6 +58,47 @@ describe("RustCrypto", () => {
5758
});
5859
});
5960

61+
describe("to-device messages", () => {
62+
let rustCrypto: RustCrypto;
63+
64+
beforeEach(async () => {
65+
const mockHttpApi = {} as MatrixHttpApi<IHttpOpts>;
66+
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
67+
});
68+
69+
it("should pass through unencrypted to-device messages", async () => {
70+
const inputs: IToDeviceEvent[] = [
71+
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
72+
];
73+
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
74+
expect(res).toEqual(inputs);
75+
});
76+
77+
it("should pass through bad encrypted messages", async () => {
78+
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
79+
const keys = olmMachine.identityKeys;
80+
const inputs: IToDeviceEvent[] = [
81+
{
82+
type: "m.room.encrypted",
83+
content: {
84+
algorithm: "m.olm.v1.curve25519-aes-sha2",
85+
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
86+
ciphertext: {
87+
[keys.curve25519.toBase64()]: {
88+
type: 0,
89+
body: "ajyjlghi",
90+
},
91+
},
92+
},
93+
sender: "@alice:example.com",
94+
},
95+
];
96+
97+
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
98+
expect(res).toEqual(inputs);
99+
});
100+
});
101+
60102
describe("outgoing requests", () => {
61103
/** the RustCrypto implementation under test */
62104
let rustCrypto: RustCrypto;

src/common-crypto/CryptoBackend.ts

+15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
18+
import type { IToDeviceEvent } from "../sync-accumulator";
1819
import { MatrixEvent } from "../models/event";
1920

2021
/**
@@ -74,6 +75,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
7475

7576
/** The methods which crypto implementations should expose to the Sync api */
7677
export interface SyncCryptoCallbacks {
78+
/**
79+
* Called by the /sync loop whenever there are incoming to-device messages.
80+
*
81+
* The implementation may preprocess the received messages (eg, decrypt them) and return an
82+
* updated list of messages for dispatch to the rest of the system.
83+
*
84+
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
85+
* messages, rather than the results of any decryption attempts.
86+
*
87+
* @param events - the received to-device messages
88+
* @returns A list of preprocessed to-device messages.
89+
*/
90+
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
91+
7792
/**
7893
* Called by the /sync loop after each /sync response is processed.
7994
*

src/crypto/index.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import { CryptoStore } from "./store/base";
8585
import { IVerificationChannel } from "./verification/request/Channel";
8686
import { TypedEventEmitter } from "../models/typed-event-emitter";
8787
import { IContent } from "../models/event";
88-
import { ISyncResponse } from "../sync-accumulator";
88+
import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
8989
import { ISignatures } from "../@types/signed";
9090
import { IMessage } from "./algorithms/olm";
9191
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
@@ -3198,6 +3198,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
31983198
}
31993199
};
32003200

3201+
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
3202+
// all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
3203+
// happens later in decryptEvent, via the EventMapper
3204+
return events.filter((toDevice) => {
3205+
if (
3206+
toDevice.type === EventType.RoomMessageEncrypted &&
3207+
!["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
3208+
) {
3209+
logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
3210+
return false;
3211+
}
3212+
return true;
3213+
});
3214+
}
3215+
32013216
private onToDeviceEvent = (event: MatrixEvent): void => {
32023217
try {
32033218
logger.log(

src/event-mapper.ts

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
6060
event.setThread(thread);
6161
}
6262

63+
// TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
64+
// to-device events), because the rust implementation decrypts to-device messages at a higher level.
65+
// Generally we probably want to use a different eventMapper implementation for to-device events because
6366
if (event.isEncrypted()) {
6467
if (!preventReEmit) {
6568
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);

src/rust-crypto/rust-crypto.ts

+20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from "@matrix-org/matrix-sdk-crypto-js";
2525

2626
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
27+
import type { IToDeviceEvent } from "../sync-accumulator";
2728
import { MatrixEvent } from "../models/event";
2829
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
2930
import { logger } from "../logger";
@@ -93,6 +94,25 @@ export class RustCrypto implements CryptoBackend {
9394
//
9495
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9596

97+
/** called by the sync loop to preprocess incoming to-device messages
98+
*
99+
* @param events - the received to-device messages
100+
* @returns A list of preprocessed to-device messages.
101+
*/
102+
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
103+
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
104+
// one-time-keys, or fallback keys, so just pass empty data.
105+
const result = await this.olmMachine.receiveSyncChanges(
106+
JSON.stringify(events),
107+
new RustSdkCryptoJs.DeviceLists(),
108+
new Map(),
109+
new Set(),
110+
);
111+
112+
// receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
113+
return JSON.parse(result);
114+
}
115+
96116
/** called by the sync loop after processing each sync.
97117
*
98118
* TODO: figure out something equivalent for sliding sync.

src/sliding-sync-sdk.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
1718
import { NotificationCountType, Room, RoomEvent } from "./models/room";
1819
import { logger } from "./logger";
1920
import * as utils from "./utils";
@@ -127,7 +128,7 @@ type ExtensionToDeviceResponse = {
127128
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
128129
private nextBatch: string | null = null;
129130

130-
public constructor(private readonly client: MatrixClient) {}
131+
public constructor(private readonly client: MatrixClient, private readonly cryptoCallbacks?: SyncCryptoCallbacks) {}
131132

132133
public name(): string {
133134
return "to_device";
@@ -150,8 +151,12 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
150151

151152
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
152153
const cancelledKeyVerificationTxns: string[] = [];
153-
data.events
154-
?.map(this.client.getEventMapper())
154+
let events = data["events"] || [];
155+
if (events.length > 0 && this.cryptoCallbacks) {
156+
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
157+
}
158+
events
159+
.map(this.client.getEventMapper())
155160
.map((toDeviceEvent) => {
156161
// map is a cheap inline forEach
157162
// We want to flag m.key.verification.start events as cancelled
@@ -373,7 +378,7 @@ export class SlidingSyncSdk {
373378
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
374379
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
375380
const extensions: Extension<any, any>[] = [
376-
new ExtensionToDevice(this.client),
381+
new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks),
377382
new ExtensionAccountData(this.client),
378383
new ExtensionTyping(this.client),
379384
new ExtensionReceipts(this.client),

src/sync.ts

+9-12
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
IStrippedState,
4949
ISyncResponse,
5050
ITimeline,
51+
IToDeviceEvent,
5152
} from "./sync-accumulator";
5253
import { MatrixEvent } from "./models/event";
5354
import { MatrixError, Method } from "./http-api";
@@ -1170,19 +1171,15 @@ export class SyncApi {
11701171
}
11711172

11721173
// handle to-device events
1173-
if (Array.isArray(data.to_device?.events) && data.to_device!.events.length > 0) {
1174-
const cancelledKeyVerificationTxns: string[] = [];
1175-
data.to_device!.events.filter((eventJSON) => {
1176-
if (
1177-
eventJSON.type === EventType.RoomMessageEncrypted &&
1178-
!["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm)
1179-
) {
1180-
logger.log("Ignoring invalid encrypted to-device event from " + eventJSON.sender);
1181-
return false;
1182-
}
1174+
if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
1175+
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events;
11831176

1184-
return true;
1185-
})
1177+
if (this.syncOpts.cryptoCallbacks) {
1178+
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
1179+
}
1180+
1181+
const cancelledKeyVerificationTxns: string[] = [];
1182+
toDeviceMessages
11861183
.map(client.getEventMapper({ toDevice: true }))
11871184
.map((toDeviceEvent) => {
11881185
// map is a cheap inline forEach

0 commit comments

Comments
 (0)