1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

notify devices when we don't send them keys (#1135)

and handle incoming notifications
This commit is contained in:
Hubert Chathi
2020-01-06 17:47:22 -05:00
committed by GitHub
parent 64fb79e0be
commit 01f6b3dfc6
9 changed files with 621 additions and 56 deletions

View File

@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -28,6 +29,8 @@ const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
import {WITHHELD_MESSAGES} from '../OlmDevice';
/**
* @private
* @constructor
@@ -47,6 +50,7 @@ function OutboundSessionInfo(sessionId) {
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
}
@@ -84,6 +88,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function(
this.sharedWithDevices[userId][deviceId] = chainIndex;
};
OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function(
userId, deviceId,
) {
if (!this.blockedDevicesNotified[userId]) {
this.blockedDevicesNotified[userId] = {};
}
this.blockedDevicesNotified[userId][deviceId] = true;
};
/**
* Determine if this session has been shared with devices which it shouldn't
* have been.
@@ -166,11 +179,14 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
* @private
*
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
*
* @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
) {
const self = this;
let session;
@@ -237,9 +253,36 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
}
return self._shareKeyWithDevices(
await self._shareKeyWithDevices(
session, shareMap,
);
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
const userBlockedDevices = blocked[userId];
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
// helper which returns the session prepared by prepareSession
@@ -363,6 +406,42 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
return mapSlices;
};
/**
* @private
*
* @param {object} devicesByUser map from userid to list of devices
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxToDeviceMessagesPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
if (currentSlice.length > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
}
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
});
}
}
if (currentSlice.length === 0) {
mapSlices.pop();
}
return mapSlices;
};
/**
* @private
*
@@ -427,6 +506,49 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
});
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {array<object>} userDeviceMap list of blocked devices to notify
*
* @param {object} payload fields to include in the notification payload
*
* @return {module:client.Promise} Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
session, userDeviceMap, payload,
) {
const contentMap = {};
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
const message = Object.assign({}, payload);
message.code = blockedInfo.code;
message.reason = blockedInfo.reason;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = message;
}
await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markNotifiedBlockedDevice(userId, deviceId);
}
}
};
/**
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
@@ -561,6 +683,42 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
}
};
/**
* Notify blocked devices that they have been blocked.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, object>} devicesByUser
* map from userid to device ID to blocked data
*/
MegolmEncryption.prototype._notifyBlockedDevices = async function(
session, devicesByUser,
) {
const payload = {
room_id: this._roomId,
session_id: session.sessionId,
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this._sendBlockedNotificationsToDevices(
session, userDeviceMaps[i], payload,
);
logger.log(`Completed blacklist notification for ${session.sessionId} `
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
logger.log(`blacklist notification for ${session.sessionId} in `
+ `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
}
}
};
/**
* @inheritdoc
*
@@ -570,42 +728,41 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
self._checkForUnknownDevices(devicesInRoom);
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
return self._ensureOutboundSession(devicesInRoom);
}).then(function(session) {
const payloadJson = {
room_id: self._roomId,
type: eventType,
content: content,
};
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
self._checkForUnknownDevices(devicesInRoom);
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
room_id: self._roomId,
type: eventType,
content: content,
};
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
session.useCount++;
return encryptedContent;
});
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
};
/**
@@ -654,8 +811,11 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
* @return {module:client.Promise} Promise which resolves to an array whose
* first element is a map from userId to deviceId to deviceInfo indicating
* the devices that messages should be encrypted to, and whose second
* element is a map from userId to deviceId to data indicating the devices
* that are in the room but that have been blocked
*/
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
@@ -676,6 +836,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
const blocked = {};
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
@@ -690,13 +851,27 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
) {
if (!blocked[userId]) {
blocked[userId] = {};
}
const blockedInfo = userDevices[deviceId].isBlocked()
? {
code: "m.blacklisted",
reason: WITHHELD_MESSAGES["m.blacklisted"],
}
: {
code: "m.unverified",
reason: WITHHELD_MESSAGES["m.unverified"],
};
blockedInfo.deviceInfo = userDevices[deviceId];
blocked[userId][deviceId] = blockedInfo;
delete userDevices[deviceId];
}
}
}
return devices;
return [devices, blocked];
};
/**
@@ -756,6 +931,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(),
);
} catch (e) {
if (e.name === "DecryptionError") {
// re-throw decryption errors as-is
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
@@ -963,6 +1143,20 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
});
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
const content = event.getContent();
await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, content.sender_key, content.session_id, content.code,
content.reason,
);
};
/**
* @inheritdoc
*/