Compare commits

...

4 Commits

5 changed files with 187 additions and 30 deletions

View File

@@ -515,9 +515,33 @@ export class StopGapWidget extends EventEmitter {
};
private onToDeviceMessage = async (payload: ReceivedToDeviceMessage): Promise<void> => {
// Check if the room the widget is in is end-to-end encrypted
let acceptEncryptedTrafficOnly: boolean;
if (this.roomId && this.client.getCrypto()) {
acceptEncryptedTrafficOnly = await this.client.getCrypto()!.isEncryptionEnabledInRoom(this.roomId);
} else {
// If the widget is not in a room, default to encrypted traffic only
acceptEncryptedTrafficOnly = true;
}
const { message, encryptionInfo } = payload;
// TODO: Update the widget API to use a proper IToDeviceMessage instead of a IRoomEvent
await this.messaging?.feedToDevice(message as IRoomEvent, encryptionInfo != null);
if (acceptEncryptedTrafficOnly) {
// Only pass on to-device messages that are encrypted
if (encryptionInfo != null) {
// TODO: Update the widget API to use a proper IToDeviceMessage instead of a IRoomEvent
await this.messaging?.feedToDevice(message as IRoomEvent, true);
} else {
logger.warn(
`Received to-device event in clear for a widget in an e2e room (${this.roomId}), dropping.`,
);
}
return;
} else {
// Forward to the widget.
// It is ok to send an encrypted to-device message even if the room is clear.
// TODO: Update the widget API to use a proper IToDeviceMessage instead of a IRoomEvent
await this.messaging?.feedToDevice(message as IRoomEvent, encryptionInfo != null);
}
};
/**

View File

@@ -424,8 +424,22 @@ export class StopGapWidgetDriver extends WidgetDriver {
): Promise<void> {
const client = MatrixClientPeg.safeGet();
if (encrypted) {
const crypto = client.getCrypto();
const crypto = client.getCrypto();
let forceEncryptedTraffic: boolean;
if (crypto) {
if (this.inRoomId) {
forceEncryptedTraffic = await client.getCrypto()!.isEncryptionEnabledInRoom(this.inRoomId);
} else {
// If the widget is not in a room, we default to only encrypted traffic
forceEncryptedTraffic = true;
}
} else {
// If the client does not have crypto we default to not allowing encrypted traffic?
forceEncryptedTraffic = false;
}
if (forceEncryptedTraffic || encrypted) {
if (!crypto) throw new Error("E2EE not enabled");
// attempt to re-batch these up into a single request

View File

@@ -158,6 +158,7 @@ export function createTestClient(): MatrixClient {
isSecretStorageReady: jest.fn().mockResolvedValue(false),
deleteKeyBackupVersion: jest.fn(),
crossSignDevice: jest.fn(),
encryptToDeviceMessages: jest.fn(),
}),
getPushActionsForEvent: jest.fn(),

View File

@@ -121,6 +121,81 @@ describe("StopGapWidget", () => {
expect(messaging.feedToDevice).toHaveBeenCalledWith(receivedToDevice.message, true);
});
it("Drop sent in clear to-device messages if room is encrypted.", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const clearReceivedToDevice = {
message: {
type: "org.example.foo",
sender: "@alice:example.org",
content: {
hello: "spoofed world",
},
},
encryptionInfo: null,
};
client.emit(ClientEvent.ReceivedToDeviceMessage, clearReceivedToDevice);
await Promise.resolve(); // flush promises
expect(messaging.feedToDevice).not.toHaveBeenCalled();
const encryptedReceivedToDevice = {
message: {
type: "org.example.foo",
sender: "@alice:example.org",
content: {
hello: "Hello world",
},
},
encryptionInfo: {
senderVerified: false,
sender: "@alice:example.org",
senderCurve25519KeyBase64: "",
senderDevice: "ABCDEFGHI",
},
};
client.emit(ClientEvent.ReceivedToDeviceMessage, encryptedReceivedToDevice);
await Promise.resolve(); // flush promises
expect(messaging.feedToDevice).toHaveBeenCalledWith(encryptedReceivedToDevice.message, true);
});
it("Default to only encrypted traffic if there is no room.", async () => {
// Replace the widget with one that has no room
// first stop messaging to clear the previous widget
widget.stopMessaging();
widget = new StopGapWidget({
app: {
id: "test",
creatorUserId: "@alice:example.org",
type: "example",
url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url&theme=$org.matrix.msc2873.client_theme",
},
// no room provided
userId: "@alice:example.org",
creatorUserId: "@alice:example.org",
waitForIframeLoad: true,
userWidget: false,
});
// Start messaging without an iframe, since ClientWidgetApi is mocked
widget.startMessaging(null as unknown as HTMLIFrameElement);
const clearReceivedToDevice = {
message: {
type: "org.example.foo",
sender: "@alice:example.org",
content: {
hello: "spoofed world",
},
},
encryptionInfo: null,
};
client.emit(ClientEvent.ReceivedToDeviceMessage, clearReceivedToDevice);
await Promise.resolve(); // flush promises
expect(messaging.feedToDevice).not.toHaveBeenCalled();
});
it("feeds incoming state updates to the widget", () => {
const event = mkEvent({
event: true,

View File

@@ -190,6 +190,22 @@ describe("StopGapWidgetDriver", () => {
beforeEach(() => {
driver = mkDefaultDriver();
mocked(client.getCrypto()!.encryptToDeviceMessages).mockImplementation(
async (eventType, devices, payload) => {
return {
eventType: "m.room.encrypted",
batch: devices.map(({ userId, deviceId }) => ({
userId,
deviceId,
payload: {
type: "m.room.encrypted",
content: { ciphertext: "ciphertext" },
},
})),
};
},
);
});
it("sends unencrypted messages", async () => {
@@ -203,25 +219,54 @@ describe("StopGapWidgetDriver", () => {
});
});
it("should force encrypted traffic if room is e2ee", async () => {
mocked(client.getCrypto()!.isEncryptionEnabledInRoom).mockResolvedValue(true);
// Try to send with `encrypted: false`, but it should be forced to true
await driver.sendToDevice("org.example.foo", false, contentMap);
expect(client.getCrypto()!.encryptToDeviceMessages).toHaveBeenCalled();
expect(client.queueToDevice).toHaveBeenCalledWith(
expect.objectContaining({
eventType: "m.room.encrypted",
}),
);
});
it("Allow to send encrypted in clear room", async () => {
mocked(client.getCrypto()!.isEncryptionEnabledInRoom).mockResolvedValue(false);
await driver.sendToDevice("org.example.foo", true, contentMap);
expect(client.getCrypto()!.encryptToDeviceMessages).toHaveBeenCalled();
expect(client.queueToDevice).toHaveBeenCalledWith(
expect.objectContaining({
eventType: "m.room.encrypted",
}),
);
});
it("Should default to encrypted traffic for non-room widgets", async () => {
const driver = new StopGapWidgetDriver(
[],
new Widget({
id: "an_id",
creatorUserId: "@alice:example.org",
type: WidgetType.CUSTOM.preferred,
url: "https://call.element.io",
}),
WidgetKind.Account,
true,
);
await driver.sendToDevice("org.example.foo", false, contentMap);
expect(client.getCrypto()!.encryptToDeviceMessages).toHaveBeenCalled();
expect(client.queueToDevice).toHaveBeenCalledWith(
expect.objectContaining({
eventType: "m.room.encrypted",
}),
);
});
it("sends encrypted messages", async () => {
const encryptToDeviceMessages = jest
.fn()
.mockImplementation(
(eventType, recipients: { userId: string; deviceId: string }[], content: object) => ({
eventType: "m.room.encrypted",
batch: recipients.map(({ userId, deviceId }) => ({
userId,
deviceId,
payload: {
eventType,
content,
},
})),
}),
);
MatrixClientPeg.safeGet().getCrypto()!.encryptToDeviceMessages = encryptToDeviceMessages;
await driver.sendToDevice("org.example.foo", true, {
"@alice:example.org": {
aliceMobile: {
@@ -235,14 +280,14 @@ describe("StopGapWidgetDriver", () => {
},
});
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
expect(client.getCrypto()!.encryptToDeviceMessages).toHaveBeenCalledWith(
"org.example.foo",
[{ deviceId: "aliceMobile", userId: "@alice:example.org" }],
{
hello: "alice",
},
);
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
expect(client.getCrypto()!.encryptToDeviceMessages).toHaveBeenCalledWith(
"org.example.foo",
[{ deviceId: "bobDesktop", userId: "@bob:example.org" }],
{
@@ -252,21 +297,19 @@ describe("StopGapWidgetDriver", () => {
expect(client.queueToDevice).toHaveBeenCalledWith({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
expect.objectContaining({
deviceId: "aliceMobile",
payload: { content: { hello: "alice" }, eventType: "org.example.foo" },
userId: "@alice:example.org",
},
}),
]),
});
expect(client.queueToDevice).toHaveBeenCalledWith({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
expect.objectContaining({
deviceId: "bobDesktop",
payload: { content: { hello: "bob" }, eventType: "org.example.foo" },
userId: "@bob:example.org",
},
}),
]),
});
});