Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Element-R: Add support for /discardsession #3209

Merged
merged 1 commit into from
Mar 29, 2023
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
59 changes: 53 additions & 6 deletions spec/integ/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ async function expectSendRoomKey(
resolve(onSendRoomKey(content));
return {};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
});
}
Expand All @@ -360,12 +365,20 @@ async function expectSendMegolmMessage(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedMessageContent = await new Promise<IContent>((resolve) => {
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (url: string, opts: RequestInit): MockResponse => {
resolve(JSON.parse(opts.body as string));
return {
event_id: "$event_id",
};
});
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
(url: string, opts: RequestInit): MockResponse => {
resolve(JSON.parse(opts.body as string));
return {
event_id: "$event_id",
};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
});

// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
Expand Down Expand Up @@ -808,6 +821,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
]);
});

it("We should start a new megolm session after forceDiscardSession", async () => {
aliceClient.setGlobalErrorOnUnknownDevices(false);
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();

// Alice shares a room with Bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
await syncPromise(aliceClient);

// Once we send the message, Alice will check Bob's device list (twice, because reasons) ...
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));

// ... and claim one of his OTKs ...
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));

// ... and send an m.room_key message
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);

// Send the first message, and check we can decrypt it.
await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendMegolmMessage(inboundGroupSessionPromise),
]);

// Finally the interesting part: discard the session.
aliceClient.forceDiscardSession(ROOM_ID);

// Now when we send the next message, we should get a *new* megolm session.
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
});

oldBackendOnly("Alice sends a megolm message", async () => {
// TODO: do something about this for the rust backend.
// Currently it fails because we don't respect the default GlobalErrorOnUnknownDevices and
Expand Down
7 changes: 4 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3122,13 +3122,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param roomId - The ID of the room to discard the session for
*
* This should not normally be necessary.
* @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}:
*
*/
public forceDiscardSession(roomId: string): void {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-End encryption disabled");
}
this.crypto.forceDiscardSession(roomId);
this.cryptoBackend.forceDiscardSession(roomId);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ export interface CryptoApi {
*/
prepareToEncrypt(room: Room): void;

/**
* Discard any existing megolm session for the given room.
*
* This will ensure that a new session is created on the next call to {@link prepareToEncrypt},
* or the next time a message is sent.
*
* This should not normally be necessary: it should only be used as a debugging tool if there has been a
* problem with encryption.
*
* @param roomId - the room to discard sessions for
*/
forceDiscardSession(roomId: string): Promise<void>;

/**
* Get a list containing all of the room keys
*
Expand Down
3 changes: 2 additions & 1 deletion src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2488,13 +2488,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* This should not normally be necessary.
*/
public forceDiscardSession(roomId: string): void {
public forceDiscardSession(roomId: string): Promise<void> {
const alg = this.roomEncryptors.get(roomId);
if (alg === undefined) throw new Error("Room not encrypted");
if (alg.forceDiscardSession === undefined) {
throw new Error("Room encryption algorithm doesn't support session discarding");
}
alg.forceDiscardSession();
return Promise.resolve();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/rust-crypto/RoomEncryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ export class RoomEncryptor {
}
}

/**
* Discard any existing group session for this room
*/
public async forceDiscardSession(): Promise<void> {
const r = await this.olmMachine.invalidateGroupSession(new RoomId(this.room.roomId));
if (r) {
this.prefixedLogger.info("Discarded existing group session");
}
}

/**
* Encrypt an event for this room
*
Expand Down
4 changes: 4 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ export class RustCrypto implements CryptoBackend {
}
}

public forceDiscardSession(roomId: string): Promise<void> {
return this.roomEncryptors[roomId]?.forceDiscardSession();
}

public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
// TODO
return [];
Expand Down