Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { decodeBase64, TypedEventEmitter } from "../../../src";
import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
import type { Logger } from "../../../src/logger.ts";
import { getParticipantId } from "../../../src/matrixrtc/utils.ts";

describe("RTCEncryptionManager", () => {
// The manager being tested
Expand Down Expand Up @@ -428,6 +429,92 @@ describe("RTCEncryptionManager", () => {
"@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 () => {
Expand Down
24 changes: 13 additions & 11 deletions src/matrixrtc/EncryptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
import { safeGetRetryAfterMs } from "../http-api/errors.ts";
import { type CallMembership } from "./CallMembership.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 {
type EnabledTransports,
Expand Down Expand Up @@ -41,14 +41,9 @@ export interface IEncryptionManager {
/**
* Retrieves the encryption keys currently managed by the encryption manager.
*
* @returns A map where the keys are identifiers and the values are arrays of
* 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.
* @returns A map of participant IDs to their encryption keys.
*/
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>>;
}

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

public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
return this.encryptionKeys;
public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
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;
Expand Down Expand Up @@ -300,7 +303,6 @@ export class EncryptionManager implements IEncryptionManager {
await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, targets);
this.logger.debug(
`sendEncryptionKeysEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`,
this.encryptionKeys,
);
} catch (error) {
if (this.keysEventUpdateTimeout === undefined) {
Expand Down
22 changes: 3 additions & 19 deletions src/matrixrtc/MatrixRTCSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,29 +516,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
* the keys.
*/
public reemitEncryptionKeys(): void {
this.encryptionManager?.getEncryptionKeys().forEach((keys, participantId) => {
keys.forEach((key, index) => {
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId);
this.encryptionManager?.getEncryptionKeys().forEach((keyRing, participantId) => {
keyRing.forEach((keyInfo) => {
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
*/
Expand Down
28 changes: 22 additions & 6 deletions src/matrixrtc/RTCEncryptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ import {
* XXX In the future we want to distribute a ratcheted key not the current one for new joiners.
*/
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
private outboundSession: OutboundEncryptionSession | null = null;

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

public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
// This is deprecated should be ignored. Only used by tests?
return new Map();
public getEncryptionKeys(): ReadonlyMap<ParticipantId, ReadonlyArray<{ key: Uint8Array; keyIndex: number }>> {
return new Map(this.participantKeyRings);
}

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 {
Expand All @@ -114,6 +128,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
public leave(): void {
this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
this.transport.stop();
this.participantKeyRings.clear();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The legacy encryption manager was only clearing its own key here, but I don't see why?

}

// Temporary for backwards compatibility
Expand All @@ -138,6 +153,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
}
}
};

/**
* 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.
Expand Down Expand Up @@ -181,7 +197,7 @@ export class RTCEncryptionManager implements IEncryptionManager {

const outdated = this.keyBuffer.isOutdated(participantId, candidateInboundSession);
if (!outdated) {
this.onEncryptionKeysChanged(
this.addKeyToParticipant(
candidateInboundSession.key,
candidateInboundSession.keyIndex,
candidateInboundSession.participantId,
Expand Down Expand Up @@ -215,7 +231,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
sharedWith: [],
keyId: 0,
};
this.onEncryptionKeysChanged(
this.addKeyToParticipant(
this.outboundSession.key,
this.outboundSession.keyId,
getParticipantId(this.userId, this.deviceId),
Expand Down Expand Up @@ -303,7 +319,7 @@ export class RTCEncryptionManager implements IEncryptionManager {
this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`);
await sleep(this.delayRolloutTimeMillis);
this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `);
this.onEncryptionKeysChanged(
this.addKeyToParticipant(
outboundKey.key,
outboundKey.keyId,
getParticipantId(this.userId, this.deviceId),
Expand Down