matrix-js-sdk 42.3.0 → 42.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/@types/json.d.ts.map +1 -1
- package/lib/@types/json.js.map +1 -1
- package/lib/client.d.ts +74 -5
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +83 -7
- package/lib/client.js.map +1 -1
- package/lib/common-crypto/CryptoBackend.d.ts +42 -23
- package/lib/common-crypto/CryptoBackend.d.ts.map +1 -1
- package/lib/common-crypto/CryptoBackend.js +7 -0
- package/lib/common-crypto/CryptoBackend.js.map +1 -1
- package/lib/embedded.d.ts +35 -1
- package/lib/embedded.d.ts.map +1 -1
- package/lib/embedded.js +49 -1
- package/lib/embedded.js.map +1 -1
- package/lib/http-api/fetch.d.ts +0 -1
- package/lib/http-api/fetch.d.ts.map +1 -1
- package/lib/http-api/fetch.js +3 -22
- package/lib/http-api/fetch.js.map +1 -1
- package/lib/http-api/logging.d.ts +10 -0
- package/lib/http-api/logging.d.ts.map +1 -0
- package/lib/http-api/logging.js +46 -0
- package/lib/http-api/logging.js.map +1 -0
- package/lib/matrixrtc/EncryptionManager.d.ts +5 -0
- package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/EncryptionManager.js.map +1 -1
- package/lib/matrixrtc/LivekitTransport.d.ts +82 -0
- package/lib/matrixrtc/LivekitTransport.d.ts.map +1 -1
- package/lib/matrixrtc/LivekitTransport.js +25 -0
- package/lib/matrixrtc/LivekitTransport.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +41 -2
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +59 -1
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +7 -4
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.d.ts +17 -0
- package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.js +68 -29
- package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
- package/lib/models/room-receipts.d.ts +9 -3
- package/lib/models/room-receipts.d.ts.map +1 -1
- package/lib/models/room-receipts.js +51 -11
- package/lib/models/room-receipts.js.map +1 -1
- package/lib/oauth/authorize.d.ts +7 -2
- package/lib/oauth/authorize.d.ts.map +1 -1
- package/lib/oauth/authorize.js +10 -4
- package/lib/oauth/authorize.js.map +1 -1
- package/lib/oauth/fetch.d.ts +17 -0
- package/lib/oauth/fetch.d.ts.map +1 -0
- package/lib/oauth/fetch.js +47 -0
- package/lib/oauth/fetch.js.map +1 -0
- package/lib/oauth/index.d.ts +15 -2
- package/lib/oauth/index.d.ts.map +1 -1
- package/lib/oauth/index.js +33 -12
- package/lib/oauth/index.js.map +1 -1
- package/lib/rust-crypto/index.d.ts +17 -0
- package/lib/rust-crypto/index.d.ts.map +1 -1
- package/lib/rust-crypto/index.js +4 -2
- package/lib/rust-crypto/index.js.map +1 -1
- package/lib/rust-crypto/rust-crypto.d.ts +6 -26
- package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
- package/lib/rust-crypto/rust-crypto.js +19 -54
- package/lib/rust-crypto/rust-crypto.js.map +1 -1
- package/lib/sliding-sync-sdk.d.ts.map +1 -1
- package/lib/sliding-sync-sdk.js +65 -27
- package/lib/sliding-sync-sdk.js.map +1 -1
- package/lib/sliding-sync.d.ts +17 -0
- package/lib/sliding-sync.d.ts.map +1 -1
- package/lib/sliding-sync.js +21 -9
- package/lib/sliding-sync.js.map +1 -1
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +54 -29
- package/lib/sync.js.map +1 -1
- package/package.json +5 -5
- package/src/@types/json.ts +11 -2
- package/src/client.ts +126 -11
- package/src/common-crypto/CryptoBackend.ts +45 -23
- package/src/embedded.ts +67 -4
- package/src/http-api/fetch.ts +2 -23
- package/src/http-api/logging.ts +46 -0
- package/src/matrixrtc/EncryptionManager.ts +6 -0
- package/src/matrixrtc/LivekitTransport.ts +86 -0
- package/src/matrixrtc/MatrixRTCSession.ts +81 -1
- package/src/matrixrtc/MembershipManager.ts +7 -7
- package/src/matrixrtc/RTCEncryptionManager.ts +80 -31
- package/src/models/room-receipts.ts +54 -10
- package/src/oauth/authorize.ts +10 -2
- package/src/oauth/fetch.ts +54 -0
- package/src/oauth/index.ts +29 -10
- package/src/rust-crypto/index.ts +23 -0
- package/src/rust-crypto/rust-crypto.ts +25 -67
- package/src/sliding-sync-sdk.ts +73 -33
- package/src/sliding-sync.ts +31 -14
- package/src/sync.ts +58 -35
package/src/rust-crypto/index.ts
CHANGED
|
@@ -103,6 +103,25 @@ export interface InitRustCryptoArgs {
|
|
|
103
103
|
* considered verified, without any manual verification taking place.
|
|
104
104
|
*/
|
|
105
105
|
caCertsPem?: string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Optional async function for signing some data with an X.509 certificate.
|
|
109
|
+
* Used to sign the user's identity so compatible clients will recognise
|
|
110
|
+
* this user as verified without manual verification taking place. If you
|
|
111
|
+
* supply this you must also supply {@link InitRustCryptoArgs#x509Validity}.
|
|
112
|
+
*/
|
|
113
|
+
x509Signer?: (item: Uint8Array) => Promise<{
|
|
114
|
+
signature_bytes: Uint8Array;
|
|
115
|
+
certificate_chain: string;
|
|
116
|
+
signature_scheme: "RsaPssSha512";
|
|
117
|
+
}>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Optional function returning the validity period of the X.509 certificate
|
|
121
|
+
* used for signing, as the number of milliseconds since the Unix epoch. If
|
|
122
|
+
* you supply this you must also supply {@link InitRustCryptoArgs#x509Signer}.
|
|
123
|
+
*/
|
|
124
|
+
x509Validity?: () => number;
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
/**
|
|
@@ -158,6 +177,8 @@ async function initOlmMachine(
|
|
|
158
177
|
legacyCryptoStore,
|
|
159
178
|
enableEncryptedStateEvents,
|
|
160
179
|
caCertsPem,
|
|
180
|
+
x509Signer,
|
|
181
|
+
x509Validity,
|
|
161
182
|
}: InitRustCryptoArgs,
|
|
162
183
|
storeHandle: StoreHandle,
|
|
163
184
|
): Promise<RustCrypto> {
|
|
@@ -169,6 +190,8 @@ async function initOlmMachine(
|
|
|
169
190
|
storeHandle,
|
|
170
191
|
logger,
|
|
171
192
|
caCertsPem,
|
|
193
|
+
x509Signer,
|
|
194
|
+
x509Validity,
|
|
172
195
|
);
|
|
173
196
|
|
|
174
197
|
// A final migration step, now that we have an OlmMachine.
|
|
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
|
|
19
19
|
|
|
20
20
|
import type { IMegolmSessionData } from "../@types/crypto.ts";
|
|
21
21
|
import { KnownMembership } from "../@types/membership.ts";
|
|
22
|
-
import { type
|
|
22
|
+
import { type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
|
|
23
23
|
import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
|
|
24
24
|
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
|
|
25
25
|
import { type Room } from "../models/room.ts";
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
DecryptionError,
|
|
31
31
|
type EventDecryptionResult,
|
|
32
32
|
type OnSyncCompletedData,
|
|
33
|
+
type SyncCryptoChanges,
|
|
33
34
|
} from "../common-crypto/CryptoBackend.ts";
|
|
34
35
|
import { type Logger, LogSpan } from "../logger.ts";
|
|
35
36
|
import { type IHttpOpts, type MatrixHttpApi, Method } from "../http-api/index.ts";
|
|
@@ -292,12 +293,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|
|
292
293
|
public async decryptEvent(event: MatrixEvent): Promise<EventDecryptionResult> {
|
|
293
294
|
const roomId = event.getRoomId();
|
|
294
295
|
if (!roomId) {
|
|
295
|
-
// presumably, a to-device message. These are normally decrypted in
|
|
296
|
+
// presumably, a to-device message. These are normally decrypted in processSyncChanges
|
|
296
297
|
// so the fact it has come back here suggests that decryption failed.
|
|
297
298
|
//
|
|
298
299
|
// once we drop support for the libolm crypto implementation, we can stop passing to-device messages
|
|
299
300
|
// through decryptEvent and hence get rid of this case.
|
|
300
|
-
throw new Error("to-device event was not decrypted in
|
|
301
|
+
throw new Error("to-device event was not decrypted in processSyncChanges");
|
|
301
302
|
}
|
|
302
303
|
return await this.eventDecryptor.attemptEventDecryption(event, this.deviceIsolationMode);
|
|
303
304
|
}
|
|
@@ -1691,41 +1692,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|
|
1691
1692
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
1692
1693
|
|
|
1693
1694
|
/**
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
* @param devices - the received device list updates
|
|
1699
|
-
* @returns A list of processed to-device messages.
|
|
1695
|
+
* Implementation of {@link SyncCryptoCallbacks.processSyncChanges}.
|
|
1696
|
+
*
|
|
1697
|
+
* Passes all of the encryption-relevant data from a sync response to the OlmMachine in a single call, and
|
|
1698
|
+
* post-processes the resulting to-device messages.
|
|
1700
1699
|
*/
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1700
|
+
public async processSyncChanges({
|
|
1701
|
+
toDeviceEvents,
|
|
1702
|
+
deviceLists,
|
|
1703
|
+
oneTimeKeysCounts,
|
|
1704
1704
|
unusedFallbackKeys,
|
|
1705
|
-
|
|
1706
|
-
}: {
|
|
1707
|
-
events
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
|
|
1712
|
-
return await this.olmMachine.receiveSyncChanges(
|
|
1713
|
-
events ? JSON.stringify(events) : "[]",
|
|
1714
|
-
devices,
|
|
1715
|
-
oneTimeKeysCounts,
|
|
1716
|
-
unusedFallbackKeys,
|
|
1705
|
+
useMsc4186 = false,
|
|
1706
|
+
}: SyncCryptoChanges): Promise<ReceivedToDeviceMessage[]> {
|
|
1707
|
+
const events = JSON.stringify(toDeviceEvents);
|
|
1708
|
+
const devices = new RustSdkCryptoJs.DeviceLists(
|
|
1709
|
+
deviceLists?.changed?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
|
|
1710
|
+
deviceLists?.left?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
|
|
1717
1711
|
);
|
|
1718
|
-
|
|
1712
|
+
const counts = new Map(Object.entries(oneTimeKeysCounts ?? {}));
|
|
1713
|
+
const fallbackKeys = unusedFallbackKeys && new Set(unusedFallbackKeys);
|
|
1719
1714
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]> {
|
|
1726
|
-
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
|
1727
|
-
// one-time-keys, or fallback keys, so just pass empty data.
|
|
1728
|
-
const processed = await this.receiveSyncChanges({ events });
|
|
1715
|
+
// The two variants differ only in how they treat a missing one-time key count: zero keys on the server (sync
|
|
1716
|
+
// v2) versus unchanged (sliding sync).
|
|
1717
|
+
const processed = useMsc4186
|
|
1718
|
+
? await this.olmMachine.receiveSyncChangesMsc4186(events, devices, counts, fallbackKeys)
|
|
1719
|
+
: await this.olmMachine.receiveSyncChanges(events, devices, counts, fallbackKeys);
|
|
1729
1720
|
|
|
1730
1721
|
const received: ReceivedToDeviceMessage[] = [];
|
|
1731
1722
|
|
|
@@ -1808,39 +1799,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|
|
1808
1799
|
return received;
|
|
1809
1800
|
}
|
|
1810
1801
|
|
|
1811
|
-
/** called by the sync loop to process one time key counts and unused fallback keys
|
|
1812
|
-
*
|
|
1813
|
-
* @param oneTimeKeysCounts - the received one time key counts
|
|
1814
|
-
* @param unusedFallbackKeys - the received unused fallback keys
|
|
1815
|
-
*/
|
|
1816
|
-
public async processKeyCounts(
|
|
1817
|
-
oneTimeKeysCounts?: Record<string, number>,
|
|
1818
|
-
unusedFallbackKeys?: string[],
|
|
1819
|
-
): Promise<void> {
|
|
1820
|
-
const mapOneTimeKeysCount = oneTimeKeysCounts && new Map<string, number>(Object.entries(oneTimeKeysCounts));
|
|
1821
|
-
const setUnusedFallbackKeys = unusedFallbackKeys && new Set<string>(unusedFallbackKeys);
|
|
1822
|
-
|
|
1823
|
-
if (mapOneTimeKeysCount !== undefined || setUnusedFallbackKeys !== undefined) {
|
|
1824
|
-
await this.receiveSyncChanges({
|
|
1825
|
-
oneTimeKeysCounts: mapOneTimeKeysCount,
|
|
1826
|
-
unusedFallbackKeys: setUnusedFallbackKeys,
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
/** called by the sync loop to process the notification that device lists have
|
|
1832
|
-
* been changed.
|
|
1833
|
-
*
|
|
1834
|
-
* @param deviceLists - device_lists field from /sync
|
|
1835
|
-
*/
|
|
1836
|
-
public async processDeviceLists(deviceLists: IDeviceLists): Promise<void> {
|
|
1837
|
-
const devices = new RustSdkCryptoJs.DeviceLists(
|
|
1838
|
-
deviceLists.changed?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
|
|
1839
|
-
deviceLists.left?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
|
|
1840
|
-
);
|
|
1841
|
-
await this.receiveSyncChanges({ devices });
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
1802
|
/** called by the sync loop on m.room.encryption events
|
|
1845
1803
|
*
|
|
1846
1804
|
* @param room - in which the event was received
|
package/src/sliding-sync-sdk.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
defaultClientOpts,
|
|
29
29
|
defaultSyncApiOpts,
|
|
30
30
|
type SetPresence,
|
|
31
|
-
|
|
31
|
+
processSyncCryptoChanges,
|
|
32
32
|
} from "./sync.ts";
|
|
33
33
|
import { type MatrixEvent } from "./models/event.ts";
|
|
34
34
|
import {
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
type IStickyStateEvent,
|
|
40
40
|
type IStrippedState,
|
|
41
41
|
type ISyncResponse,
|
|
42
|
-
type
|
|
42
|
+
type IToDeviceEvent,
|
|
43
43
|
} from "./sync-accumulator.ts";
|
|
44
44
|
import { MatrixError } from "./http-api/index.ts";
|
|
45
45
|
import {
|
|
@@ -74,8 +74,63 @@ type ExtensionE2EEResponse = Pick<
|
|
|
74
74
|
| "org.matrix.msc2732.device_unused_fallback_key_types"
|
|
75
75
|
>;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Collects the encryption-relevant parts of a sliding sync response, which arrive via two separate extensions
|
|
79
|
+
* (`e2ee` and `to_device`), so that they can be passed to the crypto layer in a single call once the whole response
|
|
80
|
+
* has been processed. See {@link SyncCryptoCallbacks.processSyncChanges} for why this matters.
|
|
81
|
+
*/
|
|
82
|
+
class E2EESyncChangesCollector {
|
|
83
|
+
private toDeviceEvents: IToDeviceEvent[] = [];
|
|
84
|
+
private e2ee?: ExtensionE2EEResponse;
|
|
85
|
+
private hasChanges = false;
|
|
86
|
+
|
|
87
|
+
public constructor(
|
|
88
|
+
private readonly client: MatrixClient,
|
|
89
|
+
private readonly cryptoCallbacks?: SyncCryptoCallbacks,
|
|
90
|
+
) {}
|
|
91
|
+
|
|
92
|
+
public onToDeviceEvents(events: IToDeviceEvent[]): void {
|
|
93
|
+
this.toDeviceEvents = events;
|
|
94
|
+
this.hasChanges = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public onE2EEChanges(data: ExtensionE2EEResponse): void {
|
|
98
|
+
this.e2ee = data;
|
|
99
|
+
this.hasChanges = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pass the collected changes to the crypto layer, and emit the resulting to-device messages on the client.
|
|
104
|
+
*
|
|
105
|
+
* A no-op if nothing has been collected since the last flush, so it is safe to call once per extension.
|
|
106
|
+
*/
|
|
107
|
+
public async flush(): Promise<void> {
|
|
108
|
+
if (!this.hasChanges) return;
|
|
109
|
+
const toDeviceEvents = this.toDeviceEvents;
|
|
110
|
+
const e2ee = this.e2ee;
|
|
111
|
+
this.toDeviceEvents = [];
|
|
112
|
+
this.e2ee = undefined;
|
|
113
|
+
this.hasChanges = false;
|
|
114
|
+
|
|
115
|
+
// Fields omitted from the `e2ee` extension are unchanged since the last response; the crypto layer knows to
|
|
116
|
+
// interpret them that way given `useMsc4186`.
|
|
117
|
+
await processSyncCryptoChanges(this.client, this.cryptoCallbacks, {
|
|
118
|
+
toDeviceEvents,
|
|
119
|
+
deviceLists: e2ee?.device_lists,
|
|
120
|
+
oneTimeKeysCounts: e2ee?.device_one_time_keys_count,
|
|
121
|
+
unusedFallbackKeys:
|
|
122
|
+
e2ee?.device_unused_fallback_key_types ?? e2ee?.["org.matrix.msc2732.device_unused_fallback_key_types"],
|
|
123
|
+
useMsc4186: true,
|
|
124
|
+
});
|
|
125
|
+
this.cryptoCallbacks?.onSyncCompleted({});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
77
129
|
class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResponse> {
|
|
78
|
-
public constructor(
|
|
130
|
+
public constructor(
|
|
131
|
+
private readonly crypto: SyncCryptoCallbacks,
|
|
132
|
+
private readonly collector: E2EESyncChangesCollector,
|
|
133
|
+
) {}
|
|
79
134
|
|
|
80
135
|
public name(): string {
|
|
81
136
|
return "e2ee";
|
|
@@ -102,18 +157,11 @@ class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResp
|
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
public async onResponse(data: ExtensionE2EEResponse): Promise<void> {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await this.crypto.processDeviceLists(data.device_lists);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Handle one_time_keys_count and unused_fallback_key_types
|
|
111
|
-
await this.crypto.processKeyCounts(
|
|
112
|
-
data.device_one_time_keys_count,
|
|
113
|
-
data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"],
|
|
114
|
-
);
|
|
160
|
+
this.collector.onE2EEChanges(data);
|
|
161
|
+
}
|
|
115
162
|
|
|
116
|
-
|
|
163
|
+
public async onResponseComplete(): Promise<void> {
|
|
164
|
+
await this.collector.flush();
|
|
117
165
|
}
|
|
118
166
|
}
|
|
119
167
|
|
|
@@ -131,10 +179,7 @@ type ExtensionToDeviceResponse = {
|
|
|
131
179
|
class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
|
|
132
180
|
private nextBatch: string | null = null;
|
|
133
181
|
|
|
134
|
-
public constructor(
|
|
135
|
-
private readonly client: MatrixClient,
|
|
136
|
-
private readonly cryptoCallbacks?: SyncCryptoCallbacks,
|
|
137
|
-
) {}
|
|
182
|
+
public constructor(private readonly collector: E2EESyncChangesCollector) {}
|
|
138
183
|
|
|
139
184
|
public name(): string {
|
|
140
185
|
return "to_device";
|
|
@@ -153,21 +198,13 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
|
|
|
153
198
|
}
|
|
154
199
|
|
|
155
200
|
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
|
|
156
|
-
|
|
157
|
-
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
|
|
158
|
-
if (this.cryptoCallbacks) {
|
|
159
|
-
receivedToDeviceMessages = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
|
|
160
|
-
} else {
|
|
161
|
-
// Crypto is not enabled, so we just return the events.
|
|
162
|
-
receivedToDeviceMessages = events.map((rawEvent) => ({
|
|
163
|
-
message: rawEvent,
|
|
164
|
-
encryptionInfo: null,
|
|
165
|
-
}));
|
|
166
|
-
}
|
|
167
|
-
processToDeviceMessages(receivedToDeviceMessages, this.client);
|
|
168
|
-
|
|
201
|
+
this.collector.onToDeviceEvents(data["events"] || []);
|
|
169
202
|
this.nextBatch = data.next_batch;
|
|
170
203
|
}
|
|
204
|
+
|
|
205
|
+
public async onResponseComplete(): Promise<void> {
|
|
206
|
+
await this.collector.flush();
|
|
207
|
+
}
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
type ExtensionAccountDataRequest = {
|
|
@@ -408,15 +445,18 @@ export class SlidingSyncSdk {
|
|
|
408
445
|
|
|
409
446
|
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
|
|
410
447
|
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
|
|
448
|
+
// The `e2ee` and `to_device` extensions feed a shared collector, so that the crypto layer sees all the
|
|
449
|
+
// encryption-relevant data from a response in a single call.
|
|
450
|
+
const e2eeCollector = new E2EESyncChangesCollector(this.client, this.syncOpts.cryptoCallbacks);
|
|
411
451
|
const extensions: Extension<any, any>[] = [
|
|
412
|
-
new ExtensionToDevice(
|
|
452
|
+
new ExtensionToDevice(e2eeCollector),
|
|
413
453
|
new ExtensionAccountData(this.client),
|
|
414
454
|
new ExtensionTyping(this.client),
|
|
415
455
|
new ExtensionReceipts(this.client),
|
|
416
456
|
new ExtensionStickyEvents(this.client),
|
|
417
457
|
];
|
|
418
458
|
if (this.syncOpts.cryptoCallbacks) {
|
|
419
|
-
extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks));
|
|
459
|
+
extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks, e2eeCollector));
|
|
420
460
|
}
|
|
421
461
|
extensions.forEach((ext) => {
|
|
422
462
|
this.slidingSync.registerExtension(ext);
|
package/src/sliding-sync.ts
CHANGED
|
@@ -249,6 +249,15 @@ export interface Extension<Req extends object, Res extends object> {
|
|
|
249
249
|
* @returns The state when it should be called.
|
|
250
250
|
*/
|
|
251
251
|
when(): ExtensionState;
|
|
252
|
+
/**
|
|
253
|
+
* An optional function which is called once per sync response, after `onResponse` has been called on all of
|
|
254
|
+
* the extensions (of the same `when()` stage) which were present in the response. It is called even if this
|
|
255
|
+
* extension was not itself present in the response.
|
|
256
|
+
*
|
|
257
|
+
* This allows extensions which need to combine their data with that of other extensions (such as `e2ee` and
|
|
258
|
+
* `to_device`) to act once the whole response has been seen.
|
|
259
|
+
*/
|
|
260
|
+
onResponseComplete?(): Promise<void>;
|
|
252
261
|
}
|
|
253
262
|
|
|
254
263
|
/**
|
|
@@ -487,23 +496,31 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|
|
487
496
|
}
|
|
488
497
|
|
|
489
498
|
private async onPreExtensionsResponse(ext: Record<string, object>): Promise<void> {
|
|
490
|
-
await
|
|
491
|
-
Object.keys(ext).map(async (extName) => {
|
|
492
|
-
if (this.extensions[extName].when() == ExtensionState.PreProcess) {
|
|
493
|
-
await this.extensions[extName].onResponse(ext[extName]);
|
|
494
|
-
}
|
|
495
|
-
}),
|
|
496
|
-
);
|
|
499
|
+
await this.onExtensionsResponse(ext, ExtensionState.PreProcess);
|
|
497
500
|
}
|
|
498
501
|
|
|
499
502
|
private async onPostExtensionsResponse(ext: Record<string, object>): Promise<void> {
|
|
500
|
-
await
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
503
|
+
await this.onExtensionsResponse(ext, ExtensionState.PostProcess);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Call `onResponse` on each registered extension of the given stage which is present in the response, and then
|
|
508
|
+
* `onResponseComplete` on every registered extension of that stage.
|
|
509
|
+
*
|
|
510
|
+
* Extensions are processed sequentially, in registration order, so that extensions whose data must be combined
|
|
511
|
+
* (such as `e2ee` and `to_device`) see a deterministic order.
|
|
512
|
+
*/
|
|
513
|
+
private async onExtensionsResponse(ext: Record<string, object>, state: ExtensionState): Promise<void> {
|
|
514
|
+
const extensions = Object.values(this.extensions).filter((extension) => extension.when() === state);
|
|
515
|
+
for (const extension of extensions) {
|
|
516
|
+
const data = ext[extension.name()];
|
|
517
|
+
if (data !== undefined) {
|
|
518
|
+
await extension.onResponse(data);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const extension of extensions) {
|
|
522
|
+
await extension.onResponseComplete?.();
|
|
523
|
+
}
|
|
507
524
|
}
|
|
508
525
|
|
|
509
526
|
/**
|
package/src/sync.ts
CHANGED
|
@@ -23,7 +23,7 @@ limitations under the License.
|
|
|
23
23
|
* for HTTP and WS at some point.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
|
|
26
|
+
import type { SyncCryptoCallbacks, SyncCryptoChanges } from "./common-crypto/CryptoBackend.ts";
|
|
27
27
|
import { type SyncUserProfile, User } from "./models/user.ts";
|
|
28
28
|
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
|
|
29
29
|
import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
|
|
@@ -1185,24 +1185,29 @@ export class SyncApi {
|
|
|
1185
1185
|
}
|
|
1186
1186
|
}
|
|
1187
1187
|
|
|
1188
|
-
//
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1188
|
+
// Handle to-device events, device list changes, one-time key counts and unused fallback keys.
|
|
1189
|
+
//
|
|
1190
|
+
// These are passed to the crypto layer together, in a single call: see the documentation of
|
|
1191
|
+
// `SyncCryptoCallbacks.processSyncChanges` for why they must not be split up. This has to happen before we
|
|
1192
|
+
// process the room events, so that any room keys received in to-device messages can be used to decrypt them.
|
|
1193
|
+
const toDeviceEvents: IToDeviceEvent[] = Array.isArray(data.to_device?.events) ? data.to_device.events : [];
|
|
1194
|
+
|
|
1195
|
+
// A cached sync (see `syncFromCache`) carries no E2EE data at all, so we skip the crypto layer for it: an
|
|
1196
|
+
// absent `device_one_time_keys_count` would otherwise be taken to mean that there are no one-time keys on the
|
|
1197
|
+
// server, triggering a spurious key upload on every restart.
|
|
1198
|
+
if (!syncEventData.fromCache) {
|
|
1199
|
+
await processSyncCryptoChanges(client, this.syncOpts.cryptoCallbacks, {
|
|
1200
|
+
toDeviceEvents,
|
|
1201
|
+
deviceLists: data.device_lists,
|
|
1202
|
+
// Per the spec, an absent `device_one_time_keys_count` means there are no one-time keys on the server.
|
|
1203
|
+
oneTimeKeysCounts: data.device_one_time_keys_count ?? {},
|
|
1204
|
+
unusedFallbackKeys:
|
|
1205
|
+
data.device_unused_fallback_key_types ??
|
|
1206
|
+
data["org.matrix.msc2732.device_unused_fallback_key_types"],
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1203
1209
|
|
|
1204
|
-
|
|
1205
|
-
} else {
|
|
1210
|
+
if (toDeviceEvents.length === 0) {
|
|
1206
1211
|
// no more to-device events: we can stop polling with a short timeout.
|
|
1207
1212
|
this.catchingUp = false;
|
|
1208
1213
|
}
|
|
@@ -1567,23 +1572,6 @@ export class SyncApi {
|
|
|
1567
1572
|
client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: true });
|
|
1568
1573
|
});
|
|
1569
1574
|
}
|
|
1570
|
-
|
|
1571
|
-
// Handle device list updates
|
|
1572
|
-
if (data.device_lists) {
|
|
1573
|
-
if (this.syncOpts.cryptoCallbacks) {
|
|
1574
|
-
await this.syncOpts.cryptoCallbacks.processDeviceLists(data.device_lists);
|
|
1575
|
-
} else {
|
|
1576
|
-
// FIXME if we *don't* have a crypto module, we still need to
|
|
1577
|
-
// invalidate the device lists. But that would require a
|
|
1578
|
-
// substantial bit of rework :/.
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Handle one_time_keys_count and unused fallback keys
|
|
1583
|
-
await this.syncOpts.cryptoCallbacks?.processKeyCounts(
|
|
1584
|
-
data.device_one_time_keys_count,
|
|
1585
|
-
data.device_unused_fallback_key_types ?? data["org.matrix.msc2732.device_unused_fallback_key_types"],
|
|
1586
|
-
);
|
|
1587
1575
|
}
|
|
1588
1576
|
|
|
1589
1577
|
/**
|
|
@@ -2001,6 +1989,41 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
|
|
|
2001
1989
|
return room;
|
|
2002
1990
|
}
|
|
2003
1991
|
|
|
1992
|
+
/**
|
|
1993
|
+
* Pass the encryption-relevant parts of a sync response to the crypto layer, and dispatch the resulting to-device
|
|
1994
|
+
* messages on the client.
|
|
1995
|
+
*
|
|
1996
|
+
* `changes.toDeviceEvents` is first filtered with {@link noUnsafeEventProps}. If crypto is not enabled, the to-device
|
|
1997
|
+
* messages are dispatched as received.
|
|
1998
|
+
*/
|
|
1999
|
+
export async function processSyncCryptoChanges(
|
|
2000
|
+
client: MatrixClient,
|
|
2001
|
+
cryptoCallbacks: SyncCryptoCallbacks | undefined,
|
|
2002
|
+
changes: SyncCryptoChanges,
|
|
2003
|
+
): Promise<void> {
|
|
2004
|
+
const toDeviceEvents = changes.toDeviceEvents.filter(noUnsafeEventProps);
|
|
2005
|
+
|
|
2006
|
+
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
|
|
2007
|
+
if (cryptoCallbacks) {
|
|
2008
|
+
try {
|
|
2009
|
+
receivedToDeviceMessages = await cryptoCallbacks.processSyncChanges({ ...changes, toDeviceEvents });
|
|
2010
|
+
} catch (e) {
|
|
2011
|
+
// Don't let a failure in the crypto layer stop the rest of the sync response from being processed: the
|
|
2012
|
+
// sync token has already been advanced, so the room data would otherwise be lost.
|
|
2013
|
+
logger.error("Error passing sync changes to the crypto layer", e);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
} else {
|
|
2017
|
+
// Crypto is not enabled, so we just return the events.
|
|
2018
|
+
//
|
|
2019
|
+
// FIXME if we *don't* have a crypto module, we still need to invalidate the device lists. But that would
|
|
2020
|
+
// require a substantial bit of rework :/.
|
|
2021
|
+
receivedToDeviceMessages = toDeviceEvents.map((message) => ({ message, encryptionInfo: null }));
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
processToDeviceMessages(receivedToDeviceMessages, client);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2004
2027
|
/**
|
|
2005
2028
|
* Process a list of (decrypted, where possible) received to-device events.
|
|
2006
2029
|
*
|