Skip to content

Commit c077201

Browse files
authored
EncryptionManager: un-deprecating EncryptionManager.getEncryptionKeys (#4912)
* EncryptionManager: should be able to re-emit keys * fix typo in test file name * review unneeded cast * remove bad comment
1 parent 53f2ad4 commit c077201

File tree

4 files changed

+125
-36
lines changed

4 files changed

+125
-36
lines changed

spec/unit/matrixrtc/RTCEncrytionManager.spec.ts renamed to spec/unit/matrixrtc/RTCEncryptionManager.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { decodeBase64, TypedEventEmitter } from "../../../src";
2525
import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
2626
import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
2727
import type { Logger } from "../../../src/logger.ts";
28+
import { getParticipantId } from "../../../src/matrixrtc/utils.ts";
2829

2930
describe("RTCEncryptionManager", () => {
3031
// The manager being tested
@@ -428,6 +429,92 @@ describe("RTCEncryptionManager", () => {
428429
"@carol:example.org:CAROLDEVICE",
429430
);
430431
});
432+
433+
it("Should store keys for later retrieval", async () => {
434+
jest.useFakeTimers();
435+
436+
const members = [
437+
aCallMembership("@bob:example.org", "BOBDEVICE"),
438+
aCallMembership("@bob:example.org", "BOBDEVICE2"),
439+
aCallMembership("@carl:example.org", "CARLDEVICE"),
440+
];
441+
getMembershipMock.mockReturnValue(members);
442+
443+
// Let's join
444+
encryptionManager.join(undefined);
445+
encryptionManager.onMembershipsUpdate(members);
446+
447+
await jest.advanceTimersByTimeAsync(10);
448+
449+
mockTransport.emit(
450+
KeyTransportEvents.ReceivedKeys,
451+
"@carl:example.org",
452+
"CARLDEVICE",
453+
"BBBBBBBBBBB",
454+
0 /* KeyId */,
455+
1000,
456+
);
457+
458+
mockTransport.emit(
459+
KeyTransportEvents.ReceivedKeys,
460+
"@carl:example.org",
461+
"CARLDEVICE",
462+
"CCCCCCCCCCC",
463+
5 /* KeyId */,
464+
1000,
465+
);
466+
467+
mockTransport.emit(
468+
KeyTransportEvents.ReceivedKeys,
469+
"@bob:example.org",
470+
"BOBDEVICE2",
471+
"DDDDDDDDDDD",
472+
0 /* KeyId */,
473+
1000,
474+
);
475+
476+
const knownKeys = encryptionManager.getEncryptionKeys();
477+
478+
// My own key should be there
479+
const myRing = knownKeys.get(getParticipantId("@alice:example.org", "DEVICE01"));
480+
expect(myRing).toBeDefined();
481+
expect(myRing).toHaveLength(1);
482+
expect(myRing![0]).toMatchObject(
483+
expect.objectContaining({
484+
keyIndex: 0,
485+
key: expect.any(Uint8Array),
486+
}),
487+
);
488+
489+
const carlRing = knownKeys.get(getParticipantId("@carl:example.org", "CARLDEVICE"));
490+
expect(carlRing).toBeDefined();
491+
expect(carlRing).toHaveLength(2);
492+
expect(carlRing![0]).toMatchObject(
493+
expect.objectContaining({
494+
keyIndex: 0,
495+
key: decodeBase64("BBBBBBBBBBB"),
496+
}),
497+
);
498+
expect(carlRing![1]).toMatchObject(
499+
expect.objectContaining({
500+
keyIndex: 5,
501+
key: decodeBase64("CCCCCCCCCCC"),
502+
}),
503+
);
504+
505+
const bobRing = knownKeys.get(getParticipantId("@bob:example.org", "BOBDEVICE2"));
506+
expect(bobRing).toBeDefined();
507+
expect(bobRing).toHaveLength(1);
508+
expect(bobRing![0]).toMatchObject(
509+
expect.objectContaining({
510+
keyIndex: 0,
511+
key: decodeBase64("DDDDDDDDDDD"),
512+
}),
513+
);
514+
515+
const bob1Ring = knownKeys.get(getParticipantId("@bob:example.org", "BOBDEVICE"));
516+
expect(bob1Ring).not.toBeDefined();
517+
});
431518
});
432519

433520
it("Should only rotate once again if several membership changes during a rollout", async () => {

src/matrixrtc/EncryptionManager.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
55
import { safeGetRetryAfterMs } from "../http-api/errors.ts";
66
import { type CallMembership } from "./CallMembership.ts";
77
import { type KeyTransportEventListener, KeyTransportEvents, type IKeyTransport } from "./IKeyTransport.ts";
8-
import { isMyMembership, type Statistics } from "./types.ts";
8+
import { isMyMembership, type ParticipantId, type Statistics } from "./types.ts";
99
import { getParticipantId } from "./utils.ts";
1010
import {
1111
type EnabledTransports,
@@ -41,14 +41,9 @@ export interface IEncryptionManager {
4141
/**
4242
* Retrieves the encryption keys currently managed by the encryption manager.
4343
*
44-
* @returns A map where the keys are identifiers and the values are arrays of
45-
* objects containing encryption keys and their associated timestamps.
46-
* @deprecated This method is used internally for testing. It is also used to re-emit keys when there is a change
47-
* of RTCSession (matrixKeyProvider#setRTCSession) -Not clear why/when switch RTCSession would occur-. Note that if we switch focus, we do keep the same RTC session,
48-
* so no need to re-emit. But it requires the encryption manager to store all keys of all participants, and this is already done
49-
* by the key provider. We don't want to add another layer of key storage.
44+
* @returns A map of participant IDs to their encryption keys.
5045
*/
51-
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
46+
getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>>;
5247
}
5348

5449
/**
@@ -104,8 +99,16 @@ export class EncryptionManager implements IEncryptionManager {
10499
this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`);
105100
}
106101

107-
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
108-
return this.encryptionKeys;
102+
public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
103+
const keysMap = new Map<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>>();
104+
for (const [userId, userKeys] of this.encryptionKeys) {
105+
const keys = userKeys.map((entry, index) => ({
106+
key: entry.key,
107+
keyIndex: index,
108+
}));
109+
keysMap.set(userId as ParticipantId, keys);
110+
}
111+
return keysMap;
109112
}
110113

111114
private joined = false;
@@ -300,7 +303,6 @@ export class EncryptionManager implements IEncryptionManager {
300303
await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, targets);
301304
this.logger.debug(
302305
`sendEncryptionKeysEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`,
303-
this.encryptionKeys,
304306
);
305307
} catch (error) {
306308
if (this.keysEventUpdateTimeout === undefined) {

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -516,29 +516,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
516516
* the keys.
517517
*/
518518
public reemitEncryptionKeys(): void {
519-
this.encryptionManager?.getEncryptionKeys().forEach((keys, participantId) => {
520-
keys.forEach((key, index) => {
521-
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId);
519+
this.encryptionManager?.getEncryptionKeys().forEach((keyRing, participantId) => {
520+
keyRing.forEach((keyInfo) => {
521+
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyInfo.key, keyInfo.keyIndex, participantId);
522522
});
523523
});
524524
}
525525

526-
/**
527-
* A map of keys used to encrypt and decrypt (we are using a symmetric
528-
* cipher) given participant's media. This also includes our own key
529-
*
530-
* @deprecated This will be made private in a future release.
531-
*/
532-
public getEncryptionKeys(): IterableIterator<[string, Array<Uint8Array>]> {
533-
const keys =
534-
this.encryptionManager?.getEncryptionKeys() ??
535-
new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
536-
// the returned array doesn't contain the timestamps
537-
return Array.from(keys.entries())
538-
.map(([participantId, keys]): [string, Uint8Array[]] => [participantId, keys.map((k) => k.key)])
539-
.values();
540-
}
541-
542526
/**
543527
* Sets a timer for the soonest membership expiry
544528
*/

src/matrixrtc/RTCEncryptionManager.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ import {
4646
* XXX In the future we want to distribute a ratcheted key not the current one for new joiners.
4747
*/
4848
export class RTCEncryptionManager implements IEncryptionManager {
49+
/**
50+
* Store the key rings for each participant.
51+
* The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.
52+
* The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.
53+
*/
54+
private participantKeyRings = new Map<ParticipantId, Array<{ key: Uint8Array; keyIndex: number }>>();
55+
4956
// The current per-sender media key for this device
5057
private outboundSession: OutboundEncryptionSession | null = null;
5158

@@ -94,9 +101,16 @@ export class RTCEncryptionManager implements IEncryptionManager {
94101
this.logger = parentLogger?.getChild(`[EncryptionManager]`);
95102
}
96103

97-
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
98-
// This is deprecated should be ignored. Only used by tests?
99-
return new Map();
104+
public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
105+
return new Map(this.participantKeyRings);
106+
}
107+
108+
private addKeyToParticipant(key: Uint8Array, keyIndex: number, participantId: ParticipantId): void {
109+
if (!this.participantKeyRings.has(participantId)) {
110+
this.participantKeyRings.set(participantId, []);
111+
}
112+
this.participantKeyRings.get(participantId)!.push({ key, keyIndex });
113+
this.onEncryptionKeysChanged(key, keyIndex, participantId);
100114
}
101115

102116
public join(joinConfig: EncryptionConfig | undefined): void {
@@ -114,6 +128,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
114128
public leave(): void {
115129
this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
116130
this.transport.stop();
131+
this.participantKeyRings.clear();
117132
}
118133

119134
// Temporary for backwards compatibility
@@ -138,6 +153,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
138153
}
139154
}
140155
};
156+
141157
/**
142158
* Will ensure that a new key is distributed and used to encrypt our media.
143159
* If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.
@@ -181,7 +197,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
181197

182198
const outdated = this.keyBuffer.isOutdated(participantId, candidateInboundSession);
183199
if (!outdated) {
184-
this.onEncryptionKeysChanged(
200+
this.addKeyToParticipant(
185201
candidateInboundSession.key,
186202
candidateInboundSession.keyIndex,
187203
candidateInboundSession.participantId,
@@ -215,7 +231,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
215231
sharedWith: [],
216232
keyId: 0,
217233
};
218-
this.onEncryptionKeysChanged(
234+
this.addKeyToParticipant(
219235
this.outboundSession.key,
220236
this.outboundSession.keyId,
221237
getParticipantId(this.userId, this.deviceId),
@@ -303,7 +319,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
303319
this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`);
304320
await sleep(this.delayRolloutTimeMillis);
305321
this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `);
306-
this.onEncryptionKeysChanged(
322+
this.addKeyToParticipant(
307323
outboundKey.key,
308324
outboundKey.keyId,
309325
getParticipantId(this.userId, this.deviceId),

0 commit comments

Comments
 (0)