You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Add support for sending user-defined encrypted to-device messages (#2528)
* Add support for sending user-defined encrypted to-device messages This is a port of the same change from the robertlong/group-call branch. * Fix tests * Expose the method in MatrixClient * Fix a code smell * Fix types * Test the MatrixClient method * Fix some types in Crypto test suite * Test the Crypto method * Fix tests * Upgrade matrix-mock-request * Move useRealTimers to afterEach
This commit is contained in:
@ -102,7 +102,7 @@
|
|||||||
"jest-localstorage-mock": "^2.4.6",
|
"jest-localstorage-mock": "^2.4.6",
|
||||||
"jest-sonar-reporter": "^2.0.0",
|
"jest-sonar-reporter": "^2.0.0",
|
||||||
"jsdoc": "^3.6.6",
|
"jsdoc": "^3.6.6",
|
||||||
"matrix-mock-request": "^2.1.1",
|
"matrix-mock-request": "^2.1.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"terser": "^5.5.1",
|
"terser": "^5.5.1",
|
||||||
"tsify": "^5.0.2",
|
"tsify": "^5.0.2",
|
||||||
|
@ -2,6 +2,7 @@ import '../olm-loader';
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import { MatrixClient } from "../../src/client";
|
||||||
import { Crypto } from "../../src/crypto";
|
import { Crypto } from "../../src/crypto";
|
||||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||||
import { MockStorageApi } from "../MockStorageApi";
|
import { MockStorageApi } from "../MockStorageApi";
|
||||||
@ -64,6 +65,10 @@ describe("Crypto", function() {
|
|||||||
return Olm.init();
|
return Olm.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("Crypto exposes the correct olm library version", function() {
|
it("Crypto exposes the correct olm library version", function() {
|
||||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||||
});
|
});
|
||||||
@ -225,8 +230,8 @@ describe("Crypto", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Key requests', function() {
|
describe('Key requests', function() {
|
||||||
let aliceClient;
|
let aliceClient: MatrixClient;
|
||||||
let bobClient;
|
let bobClient: MatrixClient;
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
aliceClient = (new TestClient(
|
aliceClient = (new TestClient(
|
||||||
@ -313,7 +318,7 @@ describe("Crypto", function() {
|
|||||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
|
|
||||||
const cryptoStore = bobClient.cryptoStore;
|
const cryptoStore = bobClient.crypto.cryptoStore;
|
||||||
const eventContent = events[0].getWireContent();
|
const eventContent = events[0].getWireContent();
|
||||||
const senderKey = eventContent.sender_key;
|
const senderKey = eventContent.sender_key;
|
||||||
const sessionId = eventContent.session_id;
|
const sessionId = eventContent.session_id;
|
||||||
@ -383,9 +388,9 @@ describe("Crypto", function() {
|
|||||||
|
|
||||||
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||||
ksEvent.getContent().sender_key = undefined; // test
|
ksEvent.getContent().sender_key = undefined; // test
|
||||||
bobClient.crypto.addInboundGroupSession = jest.fn();
|
bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn();
|
||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
expect(bobClient.crypto.addInboundGroupSession).not.toHaveBeenCalled();
|
expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||||
@ -401,7 +406,7 @@ describe("Crypto", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||||
const cryptoStore = aliceClient.cryptoStore;
|
const cryptoStore = aliceClient.crypto.cryptoStore;
|
||||||
const roomKeyRequestBody = {
|
const roomKeyRequestBody = {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: "!someroom",
|
room_id: "!someroom",
|
||||||
@ -425,7 +430,8 @@ describe("Crypto", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// replace Alice's sendToDevice function with a mock
|
// replace Alice's sendToDevice function with a mock
|
||||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
const aliceSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||||
|
aliceClient.sendToDevice = aliceSendToDevice;
|
||||||
aliceClient.startClient();
|
aliceClient.startClient();
|
||||||
|
|
||||||
// make a room key request, and record the transaction ID for the
|
// make a room key request, and record the transaction ID for the
|
||||||
@ -434,11 +440,12 @@ describe("Crypto", function() {
|
|||||||
// key requests get queued until the sync has finished, but we don't
|
// key requests get queued until the sync has finished, but we don't
|
||||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||||
// to force it to send now.
|
// to force it to send now.
|
||||||
|
// @ts-ignore
|
||||||
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||||
const txnId = aliceClient.sendToDevice.mock.calls[0][2];
|
const txnId = aliceSendToDevice.mock.calls[0][2];
|
||||||
|
|
||||||
// give the room key request manager time to update the state
|
// give the room key request manager time to update the state
|
||||||
// of the request
|
// of the request
|
||||||
@ -451,8 +458,8 @@ describe("Crypto", function() {
|
|||||||
// cancelAndResend will call sendToDevice twice:
|
// cancelAndResend will call sendToDevice twice:
|
||||||
// the first call to sendToDevice will be the cancellation
|
// the first call to sendToDevice will be the cancellation
|
||||||
// the second call to sendToDevice will be the key request
|
// the second call to sendToDevice will be the key request
|
||||||
expect(aliceClient.sendToDevice).toBeCalledTimes(3);
|
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||||
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -480,4 +487,105 @@ describe("Crypto", function() {
|
|||||||
client.stopClient();
|
client.stopClient();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("encryptAndSendToDevices", () => {
|
||||||
|
let client: TestClient;
|
||||||
|
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
|
||||||
|
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
|
||||||
|
const payload = { hello: "world" };
|
||||||
|
let encryptedPayload: object;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||||
|
ensureOlmSessionsForDevices.mockResolvedValue({});
|
||||||
|
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||||
|
encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => {
|
||||||
|
result.plaintext = JSON.stringify(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
client = new TestClient("@alice:example.org", "aliceweb");
|
||||||
|
await client.client.initCrypto();
|
||||||
|
|
||||||
|
encryptedPayload = {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
sender_key: client.client.crypto.olmDevice.deviceCurve25519Key,
|
||||||
|
ciphertext: { plaintext: JSON.stringify(payload) },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
ensureOlmSessionsForDevices.mockRestore();
|
||||||
|
encryptMessageForDevice.mockRestore();
|
||||||
|
await client.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encrypts and sends to devices", async () => {
|
||||||
|
client.httpBackend
|
||||||
|
.when("PUT", "/sendToDevice/m.room.encrypted", {
|
||||||
|
messages: {
|
||||||
|
"@bob:example.org": {
|
||||||
|
bobweb: encryptedPayload,
|
||||||
|
bobmobile: encryptedPayload,
|
||||||
|
},
|
||||||
|
"@carol:example.org": {
|
||||||
|
caroldesktop: encryptedPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.respond(200, {});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client.client.encryptAndSendToDevices(
|
||||||
|
[
|
||||||
|
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") },
|
||||||
|
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobmobile") },
|
||||||
|
{ userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") },
|
||||||
|
],
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
client.httpBackend.flushAllExpected(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends nothing to devices that couldn't be encrypted to", async () => {
|
||||||
|
encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => {
|
||||||
|
// Refuse to encrypt to Carol's desktop device
|
||||||
|
if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return;
|
||||||
|
result.plaintext = JSON.stringify(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.httpBackend
|
||||||
|
.when("PUT", "/sendToDevice/m.room.encrypted", {
|
||||||
|
// Carol is nowhere to be seen
|
||||||
|
messages: { "@bob:example.org": { bobweb: encryptedPayload } },
|
||||||
|
})
|
||||||
|
.respond(200, {});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client.client.encryptAndSendToDevices(
|
||||||
|
[
|
||||||
|
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") },
|
||||||
|
{ userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") },
|
||||||
|
],
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
client.httpBackend.flushAllExpected(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no-ops if no devices can be encrypted to", async () => {
|
||||||
|
// Refuse to encrypt to anybody
|
||||||
|
encryptMessageForDevice.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Get the room keys version request out of the way
|
||||||
|
client.httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
||||||
|
await client.httpBackend.flush("/room_keys/version", 1);
|
||||||
|
|
||||||
|
await client.client.encryptAndSendToDevices(
|
||||||
|
[{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }],
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
client.httpBackend.verifyNoOutstandingRequests();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -360,6 +360,16 @@ describe("MegolmDecryption", function() {
|
|||||||
rotation_period_ms: rotationPeriodMs,
|
rotation_period_ms: rotationPeriodMs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Splice the real method onto the mock object as megolm uses this method
|
||||||
|
// on the crypto class in order to encrypt / start sessions
|
||||||
|
// @ts-ignore Mock
|
||||||
|
mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices;
|
||||||
|
// @ts-ignore Mock
|
||||||
|
mockCrypto.olmDevice = olmDevice;
|
||||||
|
// @ts-ignore Mock
|
||||||
|
mockCrypto.baseApis = mockBaseApis;
|
||||||
|
|
||||||
mockRoom = {
|
mockRoom = {
|
||||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||||
[{ userId: "@alice:home.server" }],
|
[{ userId: "@alice:home.server" }],
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||||
} from "../../src/@types/event";
|
} from "../../src/@types/event";
|
||||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||||
|
import { Crypto } from "../../src/crypto";
|
||||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||||
import { Preset } from "../../src/@types/partials";
|
import { Preset } from "../../src/@types/partials";
|
||||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||||
@ -1297,4 +1298,19 @@ describe("MatrixClient", function() {
|
|||||||
expect(result!.aliases).toEqual(response.aliases);
|
expect(result!.aliases).toEqual(response.aliases);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("encryptAndSendToDevices", () => {
|
||||||
|
it("throws an error if crypto is unavailable", () => {
|
||||||
|
client.crypto = undefined;
|
||||||
|
expect(() => client.encryptAndSendToDevices([], {})).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is an alias for the crypto method", async () => {
|
||||||
|
client.crypto = testUtils.mock(Crypto, "Crypto");
|
||||||
|
const deviceInfos = [];
|
||||||
|
const payload = {};
|
||||||
|
await client.encryptAndSendToDevices(deviceInfos, payload);
|
||||||
|
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -40,9 +40,11 @@ import { sleep } from './utils';
|
|||||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||||
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
||||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||||
|
import { IEncryptAndSendToDevicesResult } from "./crypto";
|
||||||
import * as olmlib from "./crypto/olmlib";
|
import * as olmlib from "./crypto/olmlib";
|
||||||
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
|
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
|
||||||
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
|
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
|
||||||
|
import { IOlmDevice } from "./crypto/algorithms/megolm";
|
||||||
import { TypedReEmitter } from './ReEmitter';
|
import { TypedReEmitter } from './ReEmitter';
|
||||||
import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
@ -208,7 +210,7 @@ const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
|||||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||||
|
|
||||||
interface IExportedDevice {
|
interface IExportedDevice {
|
||||||
olmDevice: IOlmDevice;
|
olmDevice: IExportedOlmDevice;
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
}
|
}
|
||||||
@ -936,7 +938,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
protected turnServers: ITurnServer[] = [];
|
protected turnServers: ITurnServer[] = [];
|
||||||
protected turnServersExpiry = 0;
|
protected turnServersExpiry = 0;
|
||||||
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
|
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
|
||||||
protected exportedOlmDeviceToImport: IOlmDevice;
|
protected exportedOlmDeviceToImport: IExportedOlmDevice;
|
||||||
protected txnCtr = 0;
|
protected txnCtr = 0;
|
||||||
protected mediaHandler = new MediaHandler(this);
|
protected mediaHandler = new MediaHandler(this);
|
||||||
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
||||||
@ -2558,6 +2560,30 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.roomList.isRoomEncrypted(roomId);
|
return this.roomList.isRoomEncrypted(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts and sends a given object via Olm to-device messages to a given
|
||||||
|
* set of devices.
|
||||||
|
*
|
||||||
|
* @param {object[]} userDeviceInfoArr
|
||||||
|
* mapping from userId to deviceInfo
|
||||||
|
*
|
||||||
|
* @param {object} payload fields to include in the encrypted payload
|
||||||
|
* *
|
||||||
|
* @return {Promise<{contentMap, deviceInfoByDeviceId}>} 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.
|
||||||
|
*/
|
||||||
|
public encryptAndSendToDevices(
|
||||||
|
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
|
||||||
|
payload: object,
|
||||||
|
): Promise<IEncryptAndSendToDevicesResult> {
|
||||||
|
if (!this.crypto) {
|
||||||
|
throw new Error("End-to-End encryption disabled");
|
||||||
|
}
|
||||||
|
return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces the current outbound group session to be discarded such
|
* Forces the current outbound group session to be discarded such
|
||||||
* that another one will be created next time an event is sent.
|
* that another one will be created next time an event is sent.
|
||||||
|
@ -22,7 +22,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import * as olmlib from "../olmlib";
|
import * as olmlib from "../olmlib";
|
||||||
import { EventType } from '../../@types/event';
|
|
||||||
import {
|
import {
|
||||||
DecryptionAlgorithm,
|
DecryptionAlgorithm,
|
||||||
DecryptionError,
|
DecryptionError,
|
||||||
@ -38,7 +37,6 @@ import { IOlmSessionResult } from "../olmlib";
|
|||||||
import { DeviceInfoMap } from "../DeviceList";
|
import { DeviceInfoMap } from "../DeviceList";
|
||||||
import { MatrixEvent } from "../..";
|
import { MatrixEvent } from "../..";
|
||||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||||
import { ToDeviceBatch, ToDeviceMessage } from '../../models/ToDeviceMessage';
|
|
||||||
|
|
||||||
// determine whether the key can be shared with invitees
|
// determine whether the key can be shared with invitees
|
||||||
export function isRoomSharedHistory(room: Room): boolean {
|
export function isRoomSharedHistory(room: Room): boolean {
|
||||||
@ -611,87 +609,22 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
userDeviceMap: IOlmDevice[],
|
userDeviceMap: IOlmDevice[],
|
||||||
payload: IPayload,
|
payload: IPayload,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const toDeviceBatch: ToDeviceBatch = {
|
return this.crypto.encryptAndSendToDevices(
|
||||||
eventType: EventType.RoomMessageEncrypted,
|
userDeviceMap,
|
||||||
batch: [],
|
payload,
|
||||||
};
|
).then(({ toDeviceBatch, deviceInfoByUserIdAndDeviceId }) => {
|
||||||
|
// store that we successfully uploaded the keys of the current slice
|
||||||
// Map from userId to a map of deviceId to deviceInfo
|
|
||||||
const deviceInfoByUserIdAndDeviceId = new Map<string, Map<string, DeviceInfo>>();
|
|
||||||
|
|
||||||
const promises: Promise<unknown>[] = [];
|
|
||||||
for (let i = 0; i < userDeviceMap.length; i++) {
|
|
||||||
const encryptedContent: IEncryptedContent = {
|
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
|
||||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
|
||||||
ciphertext: {},
|
|
||||||
};
|
|
||||||
const val = userDeviceMap[i];
|
|
||||||
const userId = val.userId;
|
|
||||||
const deviceInfo = val.deviceInfo;
|
|
||||||
const deviceId = deviceInfo.deviceId;
|
|
||||||
|
|
||||||
// Assign to temp value to make type-checking happy
|
|
||||||
let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId);
|
|
||||||
|
|
||||||
if (userIdDeviceInfo === undefined) {
|
|
||||||
userIdDeviceInfo = new Map<string, DeviceInfo>();
|
|
||||||
|
|
||||||
deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId]
|
|
||||||
userIdDeviceInfo.set(deviceId, deviceInfo);
|
|
||||||
|
|
||||||
toDeviceBatch.batch.push({
|
|
||||||
userId,
|
|
||||||
deviceId,
|
|
||||||
payload: encryptedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
promises.push(
|
|
||||||
olmlib.encryptMessageForDevice(
|
|
||||||
encryptedContent.ciphertext,
|
|
||||||
this.userId,
|
|
||||||
this.deviceId,
|
|
||||||
this.olmDevice,
|
|
||||||
userId,
|
|
||||||
deviceInfo,
|
|
||||||
payload,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
|
||||||
// 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.
|
|
||||||
const prunedBatch: ToDeviceMessage[] = [];
|
|
||||||
for (const msg of toDeviceBatch.batch) {
|
for (const msg of toDeviceBatch.batch) {
|
||||||
if (Object.keys(msg.payload.ciphertext).length > 0) {
|
session.markSharedWithDevice(
|
||||||
prunedBatch.push(msg);
|
msg.userId,
|
||||||
} else {
|
msg.deviceId,
|
||||||
logger.log(
|
deviceInfoByUserIdAndDeviceId.get(msg.userId).get(msg.deviceId).getIdentityKey(),
|
||||||
"No ciphertext for device " +
|
chainIndex,
|
||||||
msg.userId + ":" + msg.deviceId + ": pruning",
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
toDeviceBatch.batch = prunedBatch;
|
logger.error("failed to encryptAndSendToDevices", error);
|
||||||
|
throw error;
|
||||||
return this.baseApis.queueToDevice(toDeviceBatch).then(() => {
|
|
||||||
// store that we successfully uploaded the keys of the current slice
|
|
||||||
for (const msg of toDeviceBatch.batch) {
|
|
||||||
session.markSharedWithDevice(
|
|
||||||
msg.userId,
|
|
||||||
msg.deviceId,
|
|
||||||
deviceInfoByUserIdAndDeviceId.get(msg.userId).get(msg.deviceId).getIdentityKey(),
|
|
||||||
chainIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +23,11 @@ limitations under the License.
|
|||||||
|
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
|
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
import { TypedReEmitter } from '../ReEmitter';
|
import { TypedReEmitter } from '../ReEmitter';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
||||||
|
import { IOlmDevice } from "./algorithms/megolm";
|
||||||
import * as olmlib from "./olmlib";
|
import * as olmlib from "./olmlib";
|
||||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||||
@ -68,6 +70,7 @@ import { IStore } from "../store";
|
|||||||
import { Room, RoomEvent } from "../models/room";
|
import { Room, RoomEvent } from "../models/room";
|
||||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||||
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||||
|
import { ToDeviceBatch } from "../models/ToDeviceMessage";
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
ICrossSigningKey,
|
ICrossSigningKey,
|
||||||
@ -201,6 +204,19 @@ export interface IRequestsMap {
|
|||||||
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IEncryptedContent {
|
||||||
|
algorithm: string;
|
||||||
|
sender_key: string;
|
||||||
|
ciphertext: Record<string, string>;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
export interface IEncryptAndSendToDevicesResult {
|
||||||
|
toDeviceBatch: ToDeviceBatch;
|
||||||
|
deviceInfoByUserIdAndDeviceId: Map<string, Map<string, DeviceInfo>>;
|
||||||
|
}
|
||||||
|
|
||||||
export enum CryptoEvent {
|
export enum CryptoEvent {
|
||||||
DeviceVerificationChanged = "deviceVerificationChanged",
|
DeviceVerificationChanged = "deviceVerificationChanged",
|
||||||
UserTrustStatusChanged = "userTrustStatusChanged",
|
UserTrustStatusChanged = "userTrustStatusChanged",
|
||||||
@ -3100,6 +3116,94 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts and sends a given object via Olm to-device messages to a given
|
||||||
|
* set of devices.
|
||||||
|
* @param {object[]} userDeviceInfoArr the devices to send to
|
||||||
|
* @param {object} payload fields to include in the encrypted payload
|
||||||
|
* @return {Promise<{contentMap, deviceInfoByDeviceId}>} 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.
|
||||||
|
*/
|
||||||
|
public async encryptAndSendToDevices(
|
||||||
|
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
|
||||||
|
payload: object,
|
||||||
|
): Promise<IEncryptAndSendToDevicesResult> {
|
||||||
|
const toDeviceBatch: ToDeviceBatch = {
|
||||||
|
eventType: EventType.RoomMessageEncrypted,
|
||||||
|
batch: [],
|
||||||
|
};
|
||||||
|
const deviceInfoByUserIdAndDeviceId = new Map<string, Map<string, DeviceInfo>>();
|
||||||
|
|
||||||
|
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: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign to temp value to make type-checking happy
|
||||||
|
let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId);
|
||||||
|
|
||||||
|
if (userIdDeviceInfo === undefined) {
|
||||||
|
userIdDeviceInfo = new Map<string, DeviceInfo>();
|
||||||
|
deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId]
|
||||||
|
userIdDeviceInfo.set(deviceId, deviceInfo);
|
||||||
|
|
||||||
|
toDeviceBatch.batch.push({
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
payload: encryptedContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
|
this.olmDevice,
|
||||||
|
this.baseApis,
|
||||||
|
{ [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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.baseApis.queueToDevice(toDeviceBatch);
|
||||||
|
return { toDeviceBatch, deviceInfoByUserIdAndDeviceId };
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("sendToDevice failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("encryptAndSendToDevices promises failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||||
try {
|
try {
|
||||||
this.onRoomMembership(event, member, oldMembership);
|
this.onRoomMembership(event, member, oldMembership);
|
||||||
|
21
yarn.lock
21
yarn.lock
@ -1307,7 +1307,6 @@
|
|||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz":
|
||||||
version "3.2.12"
|
version "3.2.12"
|
||||||
uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9"
|
|
||||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9"
|
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9"
|
||||||
|
|
||||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
||||||
@ -1438,9 +1437,9 @@
|
|||||||
"@octokit/openapi-types" "^12.10.0"
|
"@octokit/openapi-types" "^12.10.0"
|
||||||
|
|
||||||
"@sinclair/typebox@^0.24.1":
|
"@sinclair/typebox@^0.24.1":
|
||||||
version "0.24.20"
|
version "0.24.26"
|
||||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd"
|
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.26.tgz#84f9e8c1d93154e734a7947609a1dc7c7a81cc22"
|
||||||
integrity sha512-kVaO5aEFZb33nPMTZBxiPEkY+slxiPtqC7QX8f9B3eGOMBvEfuMfxp9DSTTCsRJPumPKjrge4yagyssO4q6qzQ==
|
integrity sha512-1ZVIyyS1NXDRVT8GjWD5jULjhDyM3IsIHef2VGUMdnWOlX2tkPjyEX/7K0TGSH2S8EaPhp1ylFdjSjUGQ+gecg==
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.8.3"
|
version "1.8.3"
|
||||||
@ -1581,9 +1580,9 @@
|
|||||||
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
|
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "18.6.1"
|
version "18.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.3.tgz#4e4a95b6fe44014563ceb514b2598b3e623d1c98"
|
||||||
integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
|
integrity sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg==
|
||||||
|
|
||||||
"@types/node@16":
|
"@types/node@16":
|
||||||
version "16.11.45"
|
version "16.11.45"
|
||||||
@ -4802,10 +4801,10 @@ matrix-events-sdk@^0.0.1-beta.7:
|
|||||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
||||||
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
||||||
|
|
||||||
matrix-mock-request@^2.1.1:
|
matrix-mock-request@^2.1.2:
|
||||||
version "2.1.1"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.1.tgz#a8fc03a2816464bb95445df4cc8885ac36786b23"
|
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.2.tgz#11e38ed1233dced88a6f2bfba1684d5c5b3aa2c2"
|
||||||
integrity sha512-CxdaUPRVB4o8JxTBMASstS2loRe+hlqeJu0Q7yyS1r36LkSSo/KAP4AuomsqxuKqaqYYnEJFJzkG0gOhxV7aqA==
|
integrity sha512-/OXCIzDGSLPJ3fs+uzDrtaOHI/Sqp4iEuniRn31U8S06mPXbvAnXknHqJ4c6A/KVwJj/nPFbGXpK4wPM038I6A==
|
||||||
dependencies:
|
dependencies:
|
||||||
expect "^28.1.0"
|
expect "^28.1.0"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user