matrix-js-sdk 41.0.0 → 41.1.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 +8 -0
- package/lib/@types/event.d.ts +1 -1
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +250 -246
- package/lib/client.js.map +1 -1
- package/lib/crypto-api/index.d.ts +13 -2
- package/lib/crypto-api/index.d.ts.map +1 -1
- package/lib/crypto-api/index.js +11 -0
- package/lib/crypto-api/index.js.map +1 -1
- package/lib/logger.d.ts +5 -5
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js.map +1 -1
- package/lib/matrixrtc/CallMembership.d.ts +49 -145
- package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
- package/lib/matrixrtc/CallMembership.js +157 -265
- package/lib/matrixrtc/CallMembership.js.map +1 -1
- package/lib/matrixrtc/EncryptionManager.d.ts +1 -85
- package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/EncryptionManager.js +0 -317
- package/lib/matrixrtc/EncryptionManager.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +18 -22
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +48 -76
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.d.ts +2 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js +3 -2
- package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +10 -4
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +10 -4
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.d.ts +6 -7
- package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
- package/lib/matrixrtc/RTCEncryptionManager.js +4 -7
- package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
- package/lib/matrixrtc/index.d.ts +1 -0
- package/lib/matrixrtc/index.d.ts.map +1 -1
- package/lib/matrixrtc/index.js.map +1 -1
- package/lib/matrixrtc/membershipData/common.d.ts +8 -0
- package/lib/matrixrtc/membershipData/common.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/common.js +26 -0
- package/lib/matrixrtc/membershipData/common.js.map +1 -0
- package/lib/matrixrtc/membershipData/index.d.ts +4 -0
- package/lib/matrixrtc/membershipData/index.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/index.js +20 -0
- package/lib/matrixrtc/membershipData/index.js.map +1 -0
- package/lib/matrixrtc/membershipData/rtc.d.ts +33 -0
- package/lib/matrixrtc/membershipData/rtc.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/rtc.js +137 -0
- package/lib/matrixrtc/membershipData/rtc.js.map +1 -0
- package/lib/matrixrtc/membershipData/session.d.ts +77 -0
- package/lib/matrixrtc/membershipData/session.d.ts.map +1 -0
- package/lib/matrixrtc/membershipData/session.js +62 -0
- package/lib/matrixrtc/membershipData/session.js.map +1 -0
- package/lib/matrixrtc/types.d.ts +23 -0
- package/lib/matrixrtc/types.d.ts.map +1 -1
- package/lib/matrixrtc/types.js +9 -1
- package/lib/matrixrtc/types.js.map +1 -1
- package/lib/matrixrtc/utils.d.ts +11 -1
- package/lib/matrixrtc/utils.d.ts.map +1 -1
- package/lib/matrixrtc/utils.js +24 -1
- package/lib/matrixrtc/utils.js.map +1 -1
- package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
- package/lib/rust-crypto/rust-crypto.js +2 -2
- package/lib/rust-crypto/rust-crypto.js.map +1 -1
- package/package.json +5 -7
- package/src/@types/event.ts +2 -2
- package/src/client.ts +5 -3
- package/src/crypto-api/index.ts +17 -2
- package/src/logger.ts +5 -5
- package/src/matrixrtc/CallMembership.ts +159 -373
- package/src/matrixrtc/EncryptionManager.ts +1 -417
- package/src/matrixrtc/MatrixRTCSession.ts +82 -122
- package/src/matrixrtc/MatrixRTCSessionManager.ts +5 -3
- package/src/matrixrtc/MembershipManager.ts +14 -17
- package/src/matrixrtc/RTCEncryptionManager.ts +7 -10
- package/src/matrixrtc/index.ts +1 -0
- package/src/matrixrtc/membershipData/common.ts +27 -0
- package/src/matrixrtc/membershipData/index.ts +19 -0
- package/src/matrixrtc/membershipData/rtc.ts +156 -0
- package/src/matrixrtc/membershipData/session.ts +146 -0
- package/src/matrixrtc/types.ts +27 -1
- package/src/matrixrtc/utils.ts +24 -2
- package/src/rust-crypto/rust-crypto.ts +4 -1
- package/lib/matrixrtc/RoomKeyTransport.d.ts +0 -25
- package/lib/matrixrtc/RoomKeyTransport.d.ts.map +0 -1
- package/lib/matrixrtc/RoomKeyTransport.js +0 -152
- package/lib/matrixrtc/RoomKeyTransport.js.map +0 -1
- package/src/matrixrtc/RoomKeyTransport.ts +0 -189
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { type Logger, logger as rootLogger } from "../logger.ts";
|
|
2
1
|
import { type EncryptionConfig } from "./MatrixRTCSession.ts";
|
|
3
|
-
import { secureRandomBase64Url } from "../randomstring.ts";
|
|
4
|
-
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
|
5
|
-
import { safeGetRetryAfterMs } from "../http-api/errors.ts";
|
|
6
2
|
import { type CallMembership } from "./CallMembership.ts";
|
|
7
|
-
import { type
|
|
8
|
-
import { isMyMembership, type EncryptionKeyMapKey, type Statistics } from "./types.ts";
|
|
3
|
+
import { type EncryptionKeyMapKey } from "./types.ts";
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* The string used for the keys in the the encryption key map.
|
|
@@ -57,414 +52,3 @@ export interface IEncryptionManager {
|
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
export type CallMembershipIdentityParts = Pick<CallMembership, "userId" | "deviceId" | "memberId">;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* This class implements the IEncryptionManager interface,
|
|
63
|
-
* and takes care of managing the encryption keys of all rtc members:
|
|
64
|
-
* - generate new keys for the local user and send them to other participants
|
|
65
|
-
* - track all keys of all other members and update livekit.
|
|
66
|
-
*
|
|
67
|
-
* @internal
|
|
68
|
-
*/
|
|
69
|
-
export class EncryptionManager implements IEncryptionManager {
|
|
70
|
-
private manageMediaKeys = false;
|
|
71
|
-
private keysEventUpdateTimeout?: ReturnType<typeof setTimeout>;
|
|
72
|
-
private makeNewKeyTimeout?: ReturnType<typeof setTimeout>;
|
|
73
|
-
private setNewKeyTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
|
74
|
-
|
|
75
|
-
private get updateEncryptionKeyThrottle(): number {
|
|
76
|
-
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private get makeKeyDelay(): number {
|
|
80
|
-
return this.joinConfig?.makeKeyDelay ?? 3_000;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private get useKeyDelay(): number {
|
|
84
|
-
return this.joinConfig?.useKeyDelay ?? 5_000;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private encryptionKeys = new Map<
|
|
88
|
-
string,
|
|
89
|
-
Array<{ key: Uint8Array<ArrayBuffer>; timestamp: number; membership: CallMembershipIdentityParts }>
|
|
90
|
-
>();
|
|
91
|
-
private lastEncryptionKeyUpdateRequest?: number;
|
|
92
|
-
|
|
93
|
-
// We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys
|
|
94
|
-
// if it looks like a membership has been updated.
|
|
95
|
-
private lastMembershipFingerprints: Set<string> | undefined;
|
|
96
|
-
|
|
97
|
-
private latestGeneratedKeyIndex = -1;
|
|
98
|
-
private joinConfig: EncryptionConfig | undefined;
|
|
99
|
-
private logger: Logger;
|
|
100
|
-
|
|
101
|
-
public constructor(
|
|
102
|
-
private membership: CallMembershipIdentityParts,
|
|
103
|
-
private getMemberships: () => CallMembership[],
|
|
104
|
-
private transport: IKeyTransport,
|
|
105
|
-
private statistics: Statistics,
|
|
106
|
-
private onEncryptionKeysChanged: (
|
|
107
|
-
keyBin: Uint8Array<ArrayBuffer>,
|
|
108
|
-
encryptionKeyIndex: number,
|
|
109
|
-
membership: CallMembershipIdentityParts,
|
|
110
|
-
rtcBackendIdentity: string,
|
|
111
|
-
) => void,
|
|
112
|
-
parentLogger?: Logger,
|
|
113
|
-
) {
|
|
114
|
-
this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private rtcBackendIdentityFromMembershipParts(membership: CallMembershipIdentityParts): string {
|
|
118
|
-
// Implement logic to construct rtcBackendIdentity from membership parts
|
|
119
|
-
return `${membership.userId}:${membership.deviceId}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
public getEncryptionKeys(): ReadonlyMap<
|
|
123
|
-
EncryptionKeyMapKey,
|
|
124
|
-
ReadonlyArray<{
|
|
125
|
-
key: Uint8Array<ArrayBuffer>;
|
|
126
|
-
keyIndex: number;
|
|
127
|
-
membership: CallMembershipIdentityParts;
|
|
128
|
-
rtcBackendIdentity: string;
|
|
129
|
-
}>
|
|
130
|
-
> {
|
|
131
|
-
const keysMap = new Map<
|
|
132
|
-
EncryptionKeyMapKey,
|
|
133
|
-
ReadonlyArray<{
|
|
134
|
-
key: Uint8Array<ArrayBuffer>;
|
|
135
|
-
keyIndex: number;
|
|
136
|
-
membership: CallMembershipIdentityParts;
|
|
137
|
-
rtcBackendIdentity: string;
|
|
138
|
-
}>
|
|
139
|
-
>();
|
|
140
|
-
for (const [userId, userKeyEntry] of this.encryptionKeys) {
|
|
141
|
-
const keys = userKeyEntry.map((entry, index) => ({
|
|
142
|
-
key: entry.key,
|
|
143
|
-
membership: entry.membership,
|
|
144
|
-
keyIndex: index,
|
|
145
|
-
rtcBackendIdentity: this.rtcBackendIdentityFromMembershipParts(entry.membership),
|
|
146
|
-
}));
|
|
147
|
-
keysMap.set(userId as EncryptionKeyMapKey, keys);
|
|
148
|
-
}
|
|
149
|
-
return keysMap;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private joined = false;
|
|
153
|
-
|
|
154
|
-
public join(joinConfig: EncryptionConfig): void {
|
|
155
|
-
this.joinConfig = joinConfig;
|
|
156
|
-
this.joined = true;
|
|
157
|
-
this.manageMediaKeys = this.joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
|
158
|
-
|
|
159
|
-
this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
|
|
160
|
-
|
|
161
|
-
this.transport.start();
|
|
162
|
-
if (this.joinConfig?.manageMediaKeys) {
|
|
163
|
-
this.makeNewSenderKey();
|
|
164
|
-
this.requestSendCurrentKey();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
public leave(): void {
|
|
169
|
-
// clear our encryption keys as we're done with them now (we'll
|
|
170
|
-
// make new keys if we rejoin). We leave keys for other participants
|
|
171
|
-
// as they may still be using the same ones.
|
|
172
|
-
this.encryptionKeys.set(getEncryptionKeyMapKey(this.membership), []);
|
|
173
|
-
this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
|
|
174
|
-
this.transport.stop();
|
|
175
|
-
|
|
176
|
-
if (this.makeNewKeyTimeout !== undefined) {
|
|
177
|
-
clearTimeout(this.makeNewKeyTimeout);
|
|
178
|
-
this.makeNewKeyTimeout = undefined;
|
|
179
|
-
}
|
|
180
|
-
for (const t of this.setNewKeyTimeouts) {
|
|
181
|
-
clearTimeout(t);
|
|
182
|
-
}
|
|
183
|
-
this.setNewKeyTimeouts.clear();
|
|
184
|
-
|
|
185
|
-
this.manageMediaKeys = false;
|
|
186
|
-
this.joined = false;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
public onMembershipsUpdate(oldMemberships: CallMembership[]): void {
|
|
190
|
-
if (this.manageMediaKeys && this.joined) {
|
|
191
|
-
const oldMembershipIds = new Set(
|
|
192
|
-
oldMemberships
|
|
193
|
-
.filter((m) => !isMyMembership(m, this.membership.userId, this.membership.deviceId))
|
|
194
|
-
.map(getEncryptionKeyMapKey),
|
|
195
|
-
);
|
|
196
|
-
const newMembershipIds = new Set(
|
|
197
|
-
this.getMemberships()
|
|
198
|
-
.filter((m) => !isMyMembership(m, this.membership.userId, this.membership.deviceId))
|
|
199
|
-
.map(getEncryptionKeyMapKey),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
|
|
203
|
-
// for this once available
|
|
204
|
-
const anyLeft = Array.from(oldMembershipIds).some((x) => !newMembershipIds.has(x));
|
|
205
|
-
const anyJoined = Array.from(newMembershipIds).some((x) => !oldMembershipIds.has(x));
|
|
206
|
-
|
|
207
|
-
const oldFingerprints = this.lastMembershipFingerprints;
|
|
208
|
-
// always store the fingerprints of these latest memberships
|
|
209
|
-
this.storeLastMembershipFingerprints();
|
|
210
|
-
|
|
211
|
-
if (anyLeft) {
|
|
212
|
-
if (this.makeNewKeyTimeout) {
|
|
213
|
-
// existing rotation in progress, so let it complete
|
|
214
|
-
} else {
|
|
215
|
-
this.logger.debug(`Member(s) have left: queueing sender key rotation`);
|
|
216
|
-
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
|
|
217
|
-
}
|
|
218
|
-
} else if (anyJoined) {
|
|
219
|
-
this.logger.debug(`New member(s) have joined: re-sending keys`);
|
|
220
|
-
this.requestSendCurrentKey();
|
|
221
|
-
} else if (oldFingerprints) {
|
|
222
|
-
// does it look like any of the members have updated their memberships?
|
|
223
|
-
const newFingerprints = this.lastMembershipFingerprints!;
|
|
224
|
-
|
|
225
|
-
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
|
|
226
|
-
// for this once available
|
|
227
|
-
const candidateUpdates =
|
|
228
|
-
Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) ||
|
|
229
|
-
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
|
|
230
|
-
if (candidateUpdates) {
|
|
231
|
-
this.logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`);
|
|
232
|
-
this.requestSendCurrentKey();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Generate a new sender key and add it at the next available index
|
|
240
|
-
* @param delayBeforeUse - If true, wait for a short period before setting the key for the
|
|
241
|
-
* media encryptor to use. If false, set the key immediately.
|
|
242
|
-
* @returns The index of the new key
|
|
243
|
-
*/
|
|
244
|
-
private makeNewSenderKey(delayBeforeUse = false): number {
|
|
245
|
-
const encryptionKey = secureRandomBase64Url(16);
|
|
246
|
-
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
|
|
247
|
-
this.logger.info("Generated new key at index " + encryptionKeyIndex);
|
|
248
|
-
this.setEncryptionKey(this.membership, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse);
|
|
249
|
-
return encryptionKeyIndex;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Requests that we resend our current keys to the room. May send a keys event immediately
|
|
254
|
-
* or queue for alter if one has already been sent recently.
|
|
255
|
-
*/
|
|
256
|
-
private requestSendCurrentKey(): void {
|
|
257
|
-
if (!this.manageMediaKeys) return;
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
this.lastEncryptionKeyUpdateRequest &&
|
|
261
|
-
this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now()
|
|
262
|
-
) {
|
|
263
|
-
this.logger.info("Last encryption key event sent too recently: postponing");
|
|
264
|
-
if (this.keysEventUpdateTimeout === undefined) {
|
|
265
|
-
this.keysEventUpdateTimeout = setTimeout(
|
|
266
|
-
() => void this.sendEncryptionKeysEvent(),
|
|
267
|
-
this.updateEncryptionKeyThrottle,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
void this.sendEncryptionKeysEvent();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Get the known encryption keys for a given participant device.
|
|
278
|
-
*
|
|
279
|
-
* @param membership - The membership identity parts of the participant
|
|
280
|
-
* @returns The encryption keys for the given participant, or undefined if they are not known.
|
|
281
|
-
*/
|
|
282
|
-
private getKeysForParticipant(membership: CallMembershipIdentityParts): Array<Uint8Array<ArrayBuffer>> | undefined {
|
|
283
|
-
return this.encryptionKeys.get(getEncryptionKeyMapKey(membership))?.map((entry) => entry.key);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Re-sends the encryption keys room event
|
|
288
|
-
*/
|
|
289
|
-
private sendEncryptionKeysEvent = async (indexToSend?: number): Promise<void> => {
|
|
290
|
-
if (this.keysEventUpdateTimeout !== undefined) {
|
|
291
|
-
clearTimeout(this.keysEventUpdateTimeout);
|
|
292
|
-
this.keysEventUpdateTimeout = undefined;
|
|
293
|
-
}
|
|
294
|
-
this.lastEncryptionKeyUpdateRequest = Date.now();
|
|
295
|
-
|
|
296
|
-
if (!this.joined) return;
|
|
297
|
-
|
|
298
|
-
const myKeys = this.getKeysForParticipant(this.membership);
|
|
299
|
-
|
|
300
|
-
if (!myKeys) {
|
|
301
|
-
this.logger.warn("Tried to send encryption keys event but no keys found!");
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (typeof indexToSend !== "number" && this.latestGeneratedKeyIndex === -1) {
|
|
306
|
-
this.logger.warn("Tried to send encryption keys event but no current key index found!");
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const keyIndexToSend = indexToSend ?? this.latestGeneratedKeyIndex;
|
|
311
|
-
|
|
312
|
-
this.logger.info(
|
|
313
|
-
`Try sending encryption keys event. keyIndexToSend=${keyIndexToSend} (method parameter: ${indexToSend})`,
|
|
314
|
-
);
|
|
315
|
-
const keyToSend = myKeys[keyIndexToSend];
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
this.statistics.counters.roomEventEncryptionKeysSent += 1;
|
|
319
|
-
const targets = this.getMemberships()
|
|
320
|
-
.filter((membership) => {
|
|
321
|
-
return membership.sender != undefined;
|
|
322
|
-
})
|
|
323
|
-
.map((membership) => {
|
|
324
|
-
return {
|
|
325
|
-
userId: membership.sender!,
|
|
326
|
-
deviceId: membership.deviceId,
|
|
327
|
-
membershipTs: membership.createdTs(),
|
|
328
|
-
};
|
|
329
|
-
});
|
|
330
|
-
await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, targets);
|
|
331
|
-
this.logger.debug(
|
|
332
|
-
`sendEncryptionKeysEvent participantId=${this.membership.userId}:${this.membership.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`,
|
|
333
|
-
);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
if (this.keysEventUpdateTimeout === undefined) {
|
|
336
|
-
const resendDelay = safeGetRetryAfterMs(error, 5000);
|
|
337
|
-
this.logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error);
|
|
338
|
-
this.keysEventUpdateTimeout = setTimeout(() => void this.sendEncryptionKeysEvent(), resendDelay);
|
|
339
|
-
} else {
|
|
340
|
-
this.logger.info("Not scheduling key resend as another re-send is already pending");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {
|
|
346
|
-
this.logger.debug(
|
|
347
|
-
`Received key over key transport ${membership.userId}:${membership.deviceId} at index ${index}`,
|
|
348
|
-
);
|
|
349
|
-
this.setEncryptionKey(membership, index, keyBase64Encoded, timestamp);
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
private storeLastMembershipFingerprints(): void {
|
|
353
|
-
this.lastMembershipFingerprints = new Set(
|
|
354
|
-
this.getMemberships()
|
|
355
|
-
.filter((m) => !isMyMembership(m, this.membership.userId, this.membership.deviceId))
|
|
356
|
-
.map((m) => `${getEncryptionKeyMapKey(m)}:${m.createdTs()}`),
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
private getNewEncryptionKeyIndex(): number {
|
|
361
|
-
if (this.latestGeneratedKeyIndex === -1) {
|
|
362
|
-
return 0;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// maximum key index is 255
|
|
366
|
-
return (this.latestGeneratedKeyIndex + 1) % 256;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Sets an encryption key at a specified index for a participant.
|
|
371
|
-
* The encryption keys for the local participant are also stored here under the
|
|
372
|
-
* user and device ID of the local participant.
|
|
373
|
-
* If the key is older than the existing key at the index, it will be ignored.
|
|
374
|
-
* @param userId - The user ID of the participant
|
|
375
|
-
* @param deviceId - Device ID of the participant
|
|
376
|
-
* @param encryptionKeyIndex - The index of the key to set
|
|
377
|
-
* @param encryptionKeyString - The string representation of the key to set in base64
|
|
378
|
-
* @param timestamp - The timestamp of the key. We assume that these are monotonic for each participant device.
|
|
379
|
-
* @param delayBeforeUse - If true, delay before emitting a key changed event. Useful when setting
|
|
380
|
-
* encryption keys for the local participant to allow time for the key to
|
|
381
|
-
* be distributed.
|
|
382
|
-
*/
|
|
383
|
-
private setEncryptionKey(
|
|
384
|
-
membership: CallMembershipIdentityParts,
|
|
385
|
-
encryptionKeyIndex: number,
|
|
386
|
-
encryptionKeyString: string,
|
|
387
|
-
timestamp: number,
|
|
388
|
-
delayBeforeUse = false,
|
|
389
|
-
): void {
|
|
390
|
-
this.logger.debug(
|
|
391
|
-
`Setting encryption key for ${membership.userId}:${membership.deviceId} at index ${encryptionKeyIndex}`,
|
|
392
|
-
);
|
|
393
|
-
const keyBin = decodeBase64(encryptionKeyString);
|
|
394
|
-
|
|
395
|
-
const mapKey = getEncryptionKeyMapKey(membership);
|
|
396
|
-
if (!this.encryptionKeys.has(mapKey)) {
|
|
397
|
-
this.encryptionKeys.set(mapKey, []);
|
|
398
|
-
}
|
|
399
|
-
const participantKeys = this.encryptionKeys.get(mapKey)!;
|
|
400
|
-
|
|
401
|
-
const existingKeyAtIndex = participantKeys[encryptionKeyIndex];
|
|
402
|
-
|
|
403
|
-
if (existingKeyAtIndex) {
|
|
404
|
-
if (existingKeyAtIndex.timestamp > timestamp) {
|
|
405
|
-
this.logger.info(
|
|
406
|
-
`Ignoring new key at index ${encryptionKeyIndex} for ${mapKey} as it is older than existing known key`,
|
|
407
|
-
);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (keysEqual(existingKeyAtIndex.key, keyBin)) {
|
|
412
|
-
existingKeyAtIndex.timestamp = timestamp;
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (membership.userId === this.membership.userId && membership.deviceId === this.membership.deviceId) {
|
|
418
|
-
// It is important to already update the latestGeneratedKeyIndex here
|
|
419
|
-
// NOT IN THE `delayBeforeUse` `setTimeout`.
|
|
420
|
-
// Even though this is where we call onEncryptionKeysChanged and set the key in EC (and livekit).
|
|
421
|
-
// It needs to happen here because we will send the key before the timeout has passed and sending
|
|
422
|
-
// the key will use latestGeneratedKeyIndex as the index. if we update it in the `setTimeout` callback
|
|
423
|
-
// it will use the wrong index (index - 1)!
|
|
424
|
-
this.latestGeneratedKeyIndex = encryptionKeyIndex;
|
|
425
|
-
}
|
|
426
|
-
participantKeys[encryptionKeyIndex] = {
|
|
427
|
-
key: keyBin,
|
|
428
|
-
timestamp,
|
|
429
|
-
membership: membership,
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
if (delayBeforeUse) {
|
|
433
|
-
const useKeyTimeout = setTimeout(() => {
|
|
434
|
-
this.setNewKeyTimeouts.delete(useKeyTimeout);
|
|
435
|
-
this.logger.info(`Delayed-emitting key changed event for ${mapKey} index ${encryptionKeyIndex}`);
|
|
436
|
-
|
|
437
|
-
this.onEncryptionKeysChanged(
|
|
438
|
-
keyBin,
|
|
439
|
-
encryptionKeyIndex,
|
|
440
|
-
membership,
|
|
441
|
-
this.rtcBackendIdentityFromMembershipParts(membership),
|
|
442
|
-
);
|
|
443
|
-
}, this.useKeyDelay);
|
|
444
|
-
this.setNewKeyTimeouts.add(useKeyTimeout);
|
|
445
|
-
} else {
|
|
446
|
-
this.onEncryptionKeysChanged(
|
|
447
|
-
keyBin,
|
|
448
|
-
encryptionKeyIndex,
|
|
449
|
-
membership,
|
|
450
|
-
this.rtcBackendIdentityFromMembershipParts(membership),
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private onRotateKeyTimeout = (): void => {
|
|
456
|
-
if (!this.manageMediaKeys) return;
|
|
457
|
-
|
|
458
|
-
this.makeNewKeyTimeout = undefined;
|
|
459
|
-
this.logger.info("Making new sender key for key rotation");
|
|
460
|
-
const newKeyIndex = this.makeNewSenderKey(true);
|
|
461
|
-
// send immediately: if we're about to start sending with a new key, it's
|
|
462
|
-
// important we get it out to others as soon as we can.
|
|
463
|
-
void this.sendEncryptionKeysEvent(newKeyIndex);
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean {
|
|
468
|
-
if (a === b) return true;
|
|
469
|
-
return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]);
|
|
470
|
-
}
|