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

Merge branch 'develop' into travis/sourcemaps

This commit is contained in:
Travis Ralston
2020-01-15 09:03:21 -07:00
10 changed files with 741 additions and 38 deletions

View File

@@ -158,6 +158,16 @@ export class DecryptionAlgorithm {
shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
/**
* Retry decrypting all the events from a sender that haven't been
* decrypted yet.
*
* @param {string} senderKey the sender's key
*/
async retryDecryptionFromSender(senderKey) {
// ignore by default
}
}
/**

View File

@@ -258,8 +258,10 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
const errorDevices = [];
await self._shareKeyWithDevices(
session, shareMap,
session, shareMap, errorDevices,
);
// are there any new blocked devices that we need to notify?
@@ -286,6 +288,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
@@ -335,6 +349,10 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
*
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
@@ -347,12 +365,16 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>}
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser,
session, chainIndex, devicemap, devicesByUser, errorDevices,
) {
const maxToDeviceMessagesPerRequest = 20;
const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored
const mapSlices = [];
@@ -382,6 +404,8 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
errorDevices.push({userId, deviceInfo});
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
@@ -391,11 +415,6 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
"share keys with device " + userId + ":" + deviceId,
);
if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
@@ -407,11 +426,25 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
entriesInCurrentSlice++;
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
}
return mapSlices;
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call.
*
* @private
*
* @param {object} devicesByUser map from userid to list of devices
@@ -419,7 +452,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxToDeviceMessagesPerRequest = 20;
const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
@@ -429,17 +462,22 @@ MegolmEncryption.prototype._splitBlockedDevices = function(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,
});
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
}
}
if (currentSlice.length === 0) {
mapSlices.pop();
@@ -537,6 +575,10 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
const message = Object.assign({}, payload);
message.code = blockedInfo.code;
message.reason = blockedInfo.reason;
if (message.code === "m.no_olm") {
delete message.room_id;
delete message.session_id;
}
if (!contentMap[userId]) {
contentMap[userId] = {};
@@ -650,8 +692,14 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*/
MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) {
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, devicesByUser, errorDevices,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
@@ -669,7 +717,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
);
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser,
session, key.chain_index, devicemap, devicesByUser, errorDevices,
);
for (let i = 0; i < userDeviceMaps.length; i++) {
@@ -741,7 +789,9 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
// 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);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom);
}
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
@@ -900,6 +950,11 @@ function MegolmDecryption(params) {
}
utils.inherits(MegolmDecryption, DecryptionAlgorithm);
const PROBLEM_DESCRIPTIONS = {
no_olm: "The sender was unable to establish a secure channel.",
unknown: "The secure channel with the sender was corrupted.",
};
/**
* @inheritdoc
*
@@ -966,7 +1021,33 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// event is still in the pending list; if not, a retry will have been
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
<<<<<<< HEAD
throw new DecryptionError(
=======
// See if there was a problem with the olm session at the time the
// event was sent. Use a fuzz factor of 2 minutes.
const problem = await this._olmDevice.sessionMayHaveProblems(
content.sender_key, event.getTs() - 120000,
);
if (problem) {
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|| PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
" Trying to create a new secure channel and re-requesting the keys.";
}
throw new base.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
problemDescription,
{
session: content.sender_key + '|' + content.session_id,
},
);
}
throw new base.DecryptionError(
>>>>>>> develop
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
@@ -1021,11 +1102,16 @@ MegolmDecryption.prototype._requestKeysForEvent = function(event) {
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = new Set();
const senderKey = content.sender_key;
const sessionId = content.session_id;
if (!this._pendingEvents[senderKey]) {
this._pendingEvents[senderKey] = new Map();
}
this._pendingEvents[k].add(event);
const senderPendingEvents = this._pendingEvents[senderKey];
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
senderPendingEvents.get(sessionId).add(event);
};
/**
@@ -1037,14 +1123,20 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
*/
MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
const senderKey = content.sender_key;
const sessionId = content.session_id;
const senderPendingEvents = this._pendingEvents[senderKey];
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
if (!pendingEvents) {
return;
}
this._pendingEvents[k].delete(event);
if (this._pendingEvents[k].size === 0) {
delete this._pendingEvents[k];
pendingEvents.delete(event);
if (pendingEvents.size === 0) {
senderPendingEvents.delete(senderKey);
}
if (senderPendingEvents.size === 0) {
delete this._pendingEvents[senderKey];
}
};
@@ -1155,11 +1247,69 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
*/
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
const content = event.getContent();
const senderKey = content.sender_key;
await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, content.sender_key, content.session_id, content.code,
content.reason,
);
if (content.code === "m.no_olm") {
const sender = event.getSender();
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
// Note: after we record that the olm session has had a problem, we
// trigger retrying decryption for all the messages from the sender's
// key, so that we can update the error message to indicate the olm
// session problem.
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
sender,
device,
{type: "m.dummy"},
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
await this._baseApis.sendToDevice("m.room.encrypted", {
[sender]: {
[device.deviceId]: encryptedContent,
},
});
} else {
await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, senderKey, content.session_id, content.code,
content.reason,
);
}
};
/**
@@ -1305,13 +1455,20 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @return {Boolean} whether all messages were successfully decrypted
*/
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
const senderPendingEvents = this._pendingEvents[senderKey];
if (!senderPendingEvents) {
return true;
}
const pending = senderPendingEvents.get(sessionId);
if (!pending) {
return true;
}
delete this._pendingEvents[k];
pending.delete(sessionId);
if (pending.size === 0) {
this._pendingEvents[senderKey];
}
await Promise.all([...pending].map(async (ev) => {
try {
@@ -1321,7 +1478,32 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
}
}));
return !this._pendingEvents[k];
// ev.attemptDecryption will re-add to this._pendingEvents if an event
// couldn't be decrypted
return !((this._pendingEvents[senderKey] || {})[sessionId]);
};
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
delete this._pendingEvents[senderKey];
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
}));
return !this._pendingEvents[senderKey];
};
registerAlgorithm(