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

Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() #4380

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca07b18
Add CryptoApi. encryptToDeviceMessages
hughns Aug 31, 2024
6a0d8e2
Overload MatrixClient. encryptAndSendToDevices instead of deprecating
hughns Sep 2, 2024
354f5b9
Revert "Overload MatrixClient. encryptAndSendToDevices instead of dep…
hughns Sep 2, 2024
28456ce
Feedback from code review
hughns Sep 2, 2024
94d68e8
Use temporary pre-release build of @matrix-org/matrix-sdk-crypto-wasm
hughns Sep 3, 2024
3266a69
Deduplicate user IDs
hughns Sep 4, 2024
8afcc05
Test for RustCrypto implementation
hughns Sep 4, 2024
7bb6a1b
Use ensureSessionsForUsers()
hughns Sep 4, 2024
687ce0d
Encrypt to-device messages in parallel
hughns Sep 6, 2024
5e978b9
Use release version of matrix-sdk-crypto-wasm
hughns Sep 6, 2024
e8bc1f5
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 9, 2024
f8a6608
Upgrade matrix-sdk-crypto-wasm to v8
hughns Sep 9, 2024
eead466
Merge branch 'hughns/matrix-sdk-crypto-wasm-8' into hughns/rust-send-…
hughns Sep 9, 2024
da0a394
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 13, 2024
6db31ab
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 18, 2024
4326169
Sync with develop
hughns Sep 18, 2024
f6df6ba
Merge branch 'develop' into hughns/rust-send-to-device
hughns Oct 7, 2024
91cab8e
Add test for olmlib CryptoApi
hughns Oct 7, 2024
b3647a8
Fix link
hughns Oct 7, 2024
f29f40a
Feedback from review
hughns Oct 11, 2024
2be86fe
Move libolm implementation to better place in file
hughns Oct 11, 2024
538b39b
FIx doc
hughns Oct 11, 2024
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#9a167f7ca220cfb7e192d5312e3159dcce5391d4",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
Expand Down
39 changes: 31 additions & 8 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3209,20 +3209,43 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Encrypts and sends a given object via Olm to-device messages to a given
* set of devices.
*
* @param userDeviceInfoArr - list of deviceInfo objects representing the devices to send to
* @param userDeviceInfoArr - @deprecated list of deviceInfo objects representing the devices to send to
* @param devices - list of (userId, deviceId) pairs objects representing the devices to send to
*
* @param payload - fields to include in the encrypted payload
*
* @returns Promise which
* resolves once the message has been encrypted and sent to the given
* userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
* of the successfully sent messages.
* @returns Promise which resolves once the payload has been encrypted and sent to the given devices
*/
public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
if (!this.crypto) {
public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void>;
public encryptAndSendToDevices(devices: { userId: string; deviceId: string }[], payload: object): Promise<void>;
hughns marked this conversation as resolved.
Show resolved Hide resolved
public async encryptAndSendToDevices(
devices: { userId: string; deviceId: string }[] | IOlmDevice<DeviceInfo>[],
payload: object,
): Promise<void> {
if (devices.length == 0) {
return;
}

if ("deviceinfo" in devices[0]) {
if (!this.crypto) {
throw new Error("End-to-End encryption disabled");
}
return this.crypto.encryptAndSendToDevices(devices as IOlmDevice<DeviceInfo>[], payload);
}

const crypto = this.getCrypto();
if (!crypto) {
throw new Error("End-to-End encryption disabled");
}
return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload);

const { type } = payload as { type: string };

const batch = await crypto.encryptToDeviceMessages(
type,
devices as { userId: string; deviceId: string }[],
payload,
);
await this.queueToDevice(batch);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import type { SecretsBundle } from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IMegolmSessionData } from "../@types/crypto.ts";
import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
import { Room } from "../models/room.ts";
import { DeviceMap } from "../models/device.ts";
import { UIAuthCallback } from "../interactive-auth.ts";
Expand Down Expand Up @@ -550,6 +551,22 @@ export interface CryptoApi {
* @param secrets - The secrets bundle received from the other device
*/
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;

/**
* Encrypts a given payload object via Olm to-device messages to a given
* set of devices.
*
* @param eventType the type of the event to send
* @param devices an array of (user ID, device ID) pairs to encrypt the payload for
* @param payload the payload to encrypt
*
* @returns a promise which resolves to the batch of encrypted payloads which can then be sent via {@link MatrixClient#queueToDevice}
hughns marked this conversation as resolved.
Show resolved Hide resolved
*/
encryptToDeviceMessages(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch>;
hughns marked this conversation as resolved.
Show resolved Hide resolved
}

/** A reason code for a failure to decrypt an event. */
Expand Down
144 changes: 94 additions & 50 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { IStore } from "../store/index.ts";
import { Room, RoomEvent } from "../models/room.ts";
import { RoomMember, RoomMemberEvent } from "../models/room-member.ts";
import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event.ts";
import { ToDeviceBatch } from "../models/ToDeviceMessage.ts";
import { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
import { ClientEvent, IKeysUploadResponse, ISignedKey, IUploadKeySignaturesResponse, MatrixClient } from "../client.ts";
import { IRoomEncryption, RoomList } from "./RoomList.ts";
import { IKeyBackupInfo } from "./keybackup.ts";
Expand Down Expand Up @@ -3522,60 +3522,17 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* resolves once the message has been encrypted and sent to the given
* userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
* of the successfully sent messages.
*
* @deprecated Instead use {@link encryptToDeviceMessages} followed by {@link MatrixClient.queueToDevice}.
*/
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
const toDeviceBatch: ToDeviceBatch = {
eventType: EventType.RoomMessageEncrypted,
batch: [],
};

try {
await Promise.all(
userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};

toDeviceBatch.batch.push({
userId,
deviceId,
payload: encryptedContent,
});

await olmlib.ensureOlmSessionsForDevices(
this.olmDevice,
this.baseApis,
new Map([[userId, [deviceInfo]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId,
this.deviceId,
this.olmDevice,
userId,
deviceInfo,
payload,
);
}),
const toDeviceBatch = await this.prepareToDeviceBatch(
EventType.RoomMessageEncrypted,
userDeviceInfoArr,
payload,
);

// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
if (Object.keys(msg.payload.ciphertext).length > 0) {
return true;
} else {
logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
return false;
}
});

try {
await this.baseApis.queueToDevice(toDeviceBatch);
} catch (e) {
Expand Down Expand Up @@ -4305,6 +4262,93 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public async startDehydration(createNewKey?: boolean): Promise<void> {
throw new Error("Not implemented");
}

private async prepareToDeviceBatch(
eventType: string,
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
payload: object,
): Promise<ToDeviceBatch> {
const toDeviceBatch: ToDeviceBatch = {
eventType,
batch: [],
};

await Promise.all(
userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};

toDeviceBatch.batch.push({
userId,
deviceId,
payload: encryptedContent,
});

await olmlib.ensureOlmSessionsForDevices(
this.olmDevice,
this.baseApis,
new Map([[userId, [deviceInfo]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId,
this.deviceId,
this.olmDevice,
userId,
deviceInfo,
payload,
);
}),
);

// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
if (Object.keys(msg.payload.ciphertext).length > 0) {
return true;
} else {
logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
return false;
}
});

return toDeviceBatch;
}

public async encryptToDeviceMessages(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch> {
const userIds = new Set(devices.map(({ userId }) => userId));
const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);

const userDeviceInfoArr: IOlmDevice<DeviceInfo>[] = [];

devices.forEach(({ userId, deviceId }) => {
const devices = deviceInfoMap.get(userId);
if (!devices) {
logger.warn(`No devices found for user ${userId}`);
return;
}

if (devices.has(deviceId)) {
// Send the message to a specific device
userDeviceInfoArr.push({ userId, deviceInfo: devices.get(deviceId)! });
} else {
logger.warn(`No device found for user ${userId} with id ${deviceId}`);
}
});

return this.prepareToDeviceBatch(eventType, userDeviceInfoArr, payload);
}
}

/**
Expand Down
28 changes: 22 additions & 6 deletions src/embedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,30 @@ export class RoomWidgetClient extends MatrixClient {
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
}

public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
public async encryptAndSendToDevices(
devices: { userId: string; deviceId: string }[] | IOlmDevice<DeviceInfo>[],
payload: object,
): Promise<void> {
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
for (const {
userId,
deviceInfo: { deviceId },
} of userDeviceInfoArr) {
contentMap.getOrCreate(userId).set(deviceId, payload);

if (devices.length == 0) {
return;
}

if ("deviceinfo" in devices[0]) {
// pre-CryptoApi style:
for (const {
userId,
deviceInfo: { deviceId },
} of devices as IOlmDevice<DeviceInfo>[]) {
contentMap.getOrCreate(userId).set(deviceId, payload);
}
} else {
// new CryptoApi style:
for (const { userId, deviceId } of devices as { userId: string; deviceId: string }[]) {
contentMap.getOrCreate(userId).set(deviceId, payload);
}
}

await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
Expand Down
32 changes: 32 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt
import { KnownMembership } from "../@types/membership.ts";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
import type { IEncryptedEventInfo } from "../crypto/api.ts";
import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
import { MatrixEvent, MatrixEventEvent } from "../models/event.ts";
import { Room } from "../models/room.ts";
import { RoomMember } from "../models/room-member.ts";
Expand Down Expand Up @@ -1713,6 +1714,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
}

public async encryptToDeviceMessages(
hughns marked this conversation as resolved.
Show resolved Hide resolved
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch> {
const batch: ToDeviceBatch = {
batch: [],
eventType,
hughns marked this conversation as resolved.
Show resolved Hide resolved
};

for (const { userId, deviceId } of devices) {
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
new RustSdkCryptoJs.UserId(userId),
new RustSdkCryptoJs.DeviceId(deviceId),
);

if (device) {
const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload));
batch.batch.push({
deviceId,
userId,
payload: encryptedPayload,
});
} else {
this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`);
}
}

return batch;
}
}

class EventDecryptor {
Expand Down
Loading