1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

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
This commit is contained in:
Valere Fedronic
2025-07-15 09:47:03 +02:00
committed by GitHub
parent 53f2ad41d6
commit c077201f2a
4 changed files with 125 additions and 36 deletions

View File

@@ -25,6 +25,7 @@ import { decodeBase64, TypedEventEmitter } from "../../../src";
import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts"; import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts"; import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
import type { Logger } from "../../../src/logger.ts"; import type { Logger } from "../../../src/logger.ts";
import { getParticipantId } from "../../../src/matrixrtc/utils.ts";
describe("RTCEncryptionManager", () => { describe("RTCEncryptionManager", () => {
// The manager being tested // The manager being tested
@@ -428,6 +429,92 @@ describe("RTCEncryptionManager", () => {
"@carol:example.org:CAROLDEVICE", "@carol:example.org:CAROLDEVICE",
); );
}); });
it("Should store keys for later retrieval", async () => {
jest.useFakeTimers();
const members = [
aCallMembership("@bob:example.org", "BOBDEVICE"),
aCallMembership("@bob:example.org", "BOBDEVICE2"),
aCallMembership("@carl:example.org", "CARLDEVICE"),
];
getMembershipMock.mockReturnValue(members);
// Let's join
encryptionManager.join(undefined);
encryptionManager.onMembershipsUpdate(members);
await jest.advanceTimersByTimeAsync(10);
mockTransport.emit(
KeyTransportEvents.ReceivedKeys,
"@carl:example.org",
"CARLDEVICE",
"BBBBBBBBBBB",
0 /* KeyId */,
1000,
);
mockTransport.emit(
KeyTransportEvents.ReceivedKeys,
"@carl:example.org",
"CARLDEVICE",
"CCCCCCCCCCC",
5 /* KeyId */,
1000,
);
mockTransport.emit(
KeyTransportEvents.ReceivedKeys,
"@bob:example.org",
"BOBDEVICE2",
"DDDDDDDDDDD",
0 /* KeyId */,
1000,
);
const knownKeys = encryptionManager.getEncryptionKeys();
// My own key should be there
const myRing = knownKeys.get(getParticipantId("@alice:example.org", "DEVICE01"));
expect(myRing).toBeDefined();
expect(myRing).toHaveLength(1);
expect(myRing![0]).toMatchObject(
expect.objectContaining({
keyIndex: 0,
key: expect.any(Uint8Array),
}),
);
const carlRing = knownKeys.get(getParticipantId("@carl:example.org", "CARLDEVICE"));
expect(carlRing).toBeDefined();
expect(carlRing).toHaveLength(2);
expect(carlRing![0]).toMatchObject(
expect.objectContaining({
keyIndex: 0,
key: decodeBase64("BBBBBBBBBBB"),
}),
);
expect(carlRing![1]).toMatchObject(
expect.objectContaining({
keyIndex: 5,
key: decodeBase64("CCCCCCCCCCC"),
}),
);
const bobRing = knownKeys.get(getParticipantId("@bob:example.org", "BOBDEVICE2"));
expect(bobRing).toBeDefined();
expect(bobRing).toHaveLength(1);
expect(bobRing![0]).toMatchObject(
expect.objectContaining({
keyIndex: 0,
key: decodeBase64("DDDDDDDDDDD"),
}),
);
const bob1Ring = knownKeys.get(getParticipantId("@bob:example.org", "BOBDEVICE"));
expect(bob1Ring).not.toBeDefined();
});
}); });
it("Should only rotate once again if several membership changes during a rollout", async () => { it("Should only rotate once again if several membership changes during a rollout", async () => {

View File

@@ -5,7 +5,7 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
import { safeGetRetryAfterMs } from "../http-api/errors.ts"; import { safeGetRetryAfterMs } from "../http-api/errors.ts";
import { type CallMembership } from "./CallMembership.ts"; import { type CallMembership } from "./CallMembership.ts";
import { type KeyTransportEventListener, KeyTransportEvents, type IKeyTransport } from "./IKeyTransport.ts"; import { type KeyTransportEventListener, KeyTransportEvents, type IKeyTransport } from "./IKeyTransport.ts";
import { isMyMembership, type Statistics } from "./types.ts"; import { isMyMembership, type ParticipantId, type Statistics } from "./types.ts";
import { getParticipantId } from "./utils.ts"; import { getParticipantId } from "./utils.ts";
import { import {
type EnabledTransports, type EnabledTransports,
@@ -41,14 +41,9 @@ export interface IEncryptionManager {
/** /**
* Retrieves the encryption keys currently managed by the encryption manager. * Retrieves the encryption keys currently managed by the encryption manager.
* *
* @returns A map where the keys are identifiers and the values are arrays of * @returns A map of participant IDs to their encryption keys.
* objects containing encryption keys and their associated timestamps.
* @deprecated This method is used internally for testing. It is also used to re-emit keys when there is a change
* 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,
* so no need to re-emit. But it requires the encryption manager to store all keys of all participants, and this is already done
* by the key provider. We don't want to add another layer of key storage.
*/ */
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>; getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>>;
} }
/** /**
@@ -104,8 +99,16 @@ export class EncryptionManager implements IEncryptionManager {
this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`); this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`);
} }
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> { public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
return this.encryptionKeys; const keysMap = new Map<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>>();
for (const [userId, userKeys] of this.encryptionKeys) {
const keys = userKeys.map((entry, index) => ({
key: entry.key,
keyIndex: index,
}));
keysMap.set(userId as ParticipantId, keys);
}
return keysMap;
} }
private joined = false; private joined = false;
@@ -300,7 +303,6 @@ export class EncryptionManager implements IEncryptionManager {
await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, targets); await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, targets);
this.logger.debug( this.logger.debug(
`sendEncryptionKeysEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`, `sendEncryptionKeysEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`,
this.encryptionKeys,
); );
} catch (error) { } catch (error) {
if (this.keysEventUpdateTimeout === undefined) { if (this.keysEventUpdateTimeout === undefined) {

View File

@@ -516,29 +516,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
* the keys. * the keys.
*/ */
public reemitEncryptionKeys(): void { public reemitEncryptionKeys(): void {
this.encryptionManager?.getEncryptionKeys().forEach((keys, participantId) => { this.encryptionManager?.getEncryptionKeys().forEach((keyRing, participantId) => {
keys.forEach((key, index) => { keyRing.forEach((keyInfo) => {
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId); this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyInfo.key, keyInfo.keyIndex, participantId);
}); });
}); });
} }
/**
* A map of keys used to encrypt and decrypt (we are using a symmetric
* cipher) given participant's media. This also includes our own key
*
* @deprecated This will be made private in a future release.
*/
public getEncryptionKeys(): IterableIterator<[string, Array<Uint8Array>]> {
const keys =
this.encryptionManager?.getEncryptionKeys() ??
new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
// the returned array doesn't contain the timestamps
return Array.from(keys.entries())
.map(([participantId, keys]): [string, Uint8Array[]] => [participantId, keys.map((k) => k.key)])
.values();
}
/** /**
* Sets a timer for the soonest membership expiry * Sets a timer for the soonest membership expiry
*/ */

View File

@@ -46,6 +46,13 @@ import {
* XXX In the future we want to distribute a ratcheted key not the current one for new joiners. * XXX In the future we want to distribute a ratcheted key not the current one for new joiners.
*/ */
export class RTCEncryptionManager implements IEncryptionManager { export class RTCEncryptionManager implements IEncryptionManager {
/**
* Store the key rings for each participant.
* The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.
* The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.
*/
private participantKeyRings = new Map<ParticipantId, Array<{ key: Uint8Array; keyIndex: number }>>();
// The current per-sender media key for this device // The current per-sender media key for this device
private outboundSession: OutboundEncryptionSession | null = null; private outboundSession: OutboundEncryptionSession | null = null;
@@ -94,9 +101,16 @@ export class RTCEncryptionManager implements IEncryptionManager {
this.logger = parentLogger?.getChild(`[EncryptionManager]`); this.logger = parentLogger?.getChild(`[EncryptionManager]`);
} }
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> { public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
// This is deprecated should be ignored. Only used by tests? return new Map(this.participantKeyRings);
return new Map(); }
private addKeyToParticipant(key: Uint8Array, keyIndex: number, participantId: ParticipantId): void {
if (!this.participantKeyRings.has(participantId)) {
this.participantKeyRings.set(participantId, []);
}
this.participantKeyRings.get(participantId)!.push({ key, keyIndex });
this.onEncryptionKeysChanged(key, keyIndex, participantId);
} }
public join(joinConfig: EncryptionConfig | undefined): void { public join(joinConfig: EncryptionConfig | undefined): void {
@@ -114,6 +128,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
public leave(): void { public leave(): void {
this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived); this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
this.transport.stop(); this.transport.stop();
this.participantKeyRings.clear();
} }
// Temporary for backwards compatibility // Temporary for backwards compatibility
@@ -138,6 +153,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
} }
} }
}; };
/** /**
* Will ensure that a new key is distributed and used to encrypt our media. * Will ensure that a new key is distributed and used to encrypt our media.
* If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed. * 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 {
const outdated = this.keyBuffer.isOutdated(participantId, candidateInboundSession); const outdated = this.keyBuffer.isOutdated(participantId, candidateInboundSession);
if (!outdated) { if (!outdated) {
this.onEncryptionKeysChanged( this.addKeyToParticipant(
candidateInboundSession.key, candidateInboundSession.key,
candidateInboundSession.keyIndex, candidateInboundSession.keyIndex,
candidateInboundSession.participantId, candidateInboundSession.participantId,
@@ -215,7 +231,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
sharedWith: [], sharedWith: [],
keyId: 0, keyId: 0,
}; };
this.onEncryptionKeysChanged( this.addKeyToParticipant(
this.outboundSession.key, this.outboundSession.key,
this.outboundSession.keyId, this.outboundSession.keyId,
getParticipantId(this.userId, this.deviceId), getParticipantId(this.userId, this.deviceId),
@@ -303,7 +319,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`); this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`);
await sleep(this.delayRolloutTimeMillis); await sleep(this.delayRolloutTimeMillis);
this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `); this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `);
this.onEncryptionKeysChanged( this.addKeyToParticipant(
outboundKey.key, outboundKey.key,
outboundKey.keyId, outboundKey.keyId,
getParticipantId(this.userId, this.deviceId), getParticipantId(this.userId, this.deviceId),