You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Send a room key request on decryption failure
When we are missing the keys to decrypt an event, send out a request for those keys to our other devices and to the original sender.
This commit is contained in:
217
src/crypto/OutgoingRoomKeyRequestManager.js
Normal file
217
src/crypto/OutgoingRoomKeyRequestManager.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal module. Management of outgoing room key requests.
|
||||||
|
*
|
||||||
|
* See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
|
||||||
|
* for draft documentation on what we're supposed to be implementing here.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// delay between deciding we want some keys, and sending out the request, to
|
||||||
|
// allow for (a) it turning up anyway, (b) grouping requests together
|
||||||
|
const SEND_KEY_REQUESTS_DELAY_MS = 500;
|
||||||
|
|
||||||
|
/** possible states for a room key request
|
||||||
|
*
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
const ROOM_KEY_REQUEST_STATES = {
|
||||||
|
/** request not yet sent */
|
||||||
|
UNSENT: 0,
|
||||||
|
|
||||||
|
/** request sent, awaiting reply */
|
||||||
|
SENT: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class OutgoingRoomKeyRequestManager {
|
||||||
|
constructor(baseApis, deviceId, cryptoStore) {
|
||||||
|
this._baseApis = baseApis;
|
||||||
|
this._deviceId = deviceId;
|
||||||
|
this._cryptoStore = cryptoStore;
|
||||||
|
|
||||||
|
// handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null
|
||||||
|
// if the callback has been set, or if it is still running.
|
||||||
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
|
|
||||||
|
// sanity check to ensure that we don't end up with two concurrent runs
|
||||||
|
// of _sendOutgoingRoomKeyRequests
|
||||||
|
this._sendOutgoingRoomKeyRequestsRunning = false;
|
||||||
|
|
||||||
|
this._clientRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the client is started. Sets background processes running.
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
this._clientRunning = true;
|
||||||
|
|
||||||
|
// set the timer going, to handle any requests which didn't get sent
|
||||||
|
// on the previous run of the client.
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the client is stopped. Stops any running background processes.
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
// stop the timer on the next run
|
||||||
|
this._clientRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send off a room key request, if we haven't already done so.
|
||||||
|
*
|
||||||
|
* The `requestBody` is compared (with a deep-equality check) against
|
||||||
|
* previous queued or sent requests and if it matches, no change is made.
|
||||||
|
* Otherwise, a request is added to the pending list, and a job is started
|
||||||
|
* in the background to send it.
|
||||||
|
*
|
||||||
|
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||||
|
* @param {Array<{userId: string, deviceId: string}>} recipients
|
||||||
|
*
|
||||||
|
* @returns {Promise} resolves when the request has been added to the
|
||||||
|
* pending list (or we have established that a similar request already
|
||||||
|
* exists)
|
||||||
|
*/
|
||||||
|
sendRoomKeyRequest(requestBody, recipients) {
|
||||||
|
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
|
||||||
|
requestBody: requestBody,
|
||||||
|
recipients: recipients,
|
||||||
|
requestId: this._baseApis.makeTxnId(),
|
||||||
|
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||||
|
}).then((req) => {
|
||||||
|
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the background timer to send queued requests, if the timer isn't
|
||||||
|
// already running
|
||||||
|
_startTimer() {
|
||||||
|
if (this._sendOutgoingRoomKeyRequestsTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSendingOutgoingRoomKeyRequests = () => {
|
||||||
|
if (this._sendOutgoingRoomKeyRequestsRunning) {
|
||||||
|
throw new Error("RoomKeyRequestSend already in progress!");
|
||||||
|
}
|
||||||
|
this._sendOutgoingRoomKeyRequestsRunning = true;
|
||||||
|
|
||||||
|
this._sendOutgoingRoomKeyRequests().finally(() => {
|
||||||
|
this._sendOutgoingRoomKeyRequestsRunning = false;
|
||||||
|
}).done();
|
||||||
|
};
|
||||||
|
|
||||||
|
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||||
|
startSendingOutgoingRoomKeyRequests,
|
||||||
|
SEND_KEY_REQUESTS_DELAY_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for and send any queued requests. Runs itself recursively until
|
||||||
|
// there are no more requests, or there is an error (in which case, the
|
||||||
|
// timer will be restarted before the promise resolves).
|
||||||
|
_sendOutgoingRoomKeyRequests() {
|
||||||
|
if (!this._clientRunning) {
|
||||||
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
|
return q();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Looking for queued outgoing room key requests");
|
||||||
|
|
||||||
|
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||||
|
ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||||
|
]).then((req) => {
|
||||||
|
if (!req) {
|
||||||
|
console.log("No more outgoing room key requests");
|
||||||
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._sendOutgoingRoomKeyRequest(req).then(() => {
|
||||||
|
// go around the loop again
|
||||||
|
return this._sendOutgoingRoomKeyRequests();
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("Error sending room key request; will retry later.", e);
|
||||||
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
|
this._startTimer();
|
||||||
|
}).done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a RoomKeyRequest, send it and update the request record
|
||||||
|
_sendOutgoingRoomKeyRequest(req) {
|
||||||
|
console.log(
|
||||||
|
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
|
||||||
|
` from ${stringifyRecipientList(req.recipients)}` +
|
||||||
|
`(id ${req.requestId})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestMessage = {
|
||||||
|
action: "request",
|
||||||
|
requesting_device_id: this._deviceId,
|
||||||
|
request_id: req.requestId,
|
||||||
|
body: req.requestBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._sendMessageToDevices(
|
||||||
|
requestMessage, req.recipients, req.requestId,
|
||||||
|
).then(() => {
|
||||||
|
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||||
|
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||||
|
{ state: ROOM_KEY_REQUEST_STATES.SENT },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a RoomKeyRequest to a list of recipients
|
||||||
|
_sendMessageToDevices(message, recipients, txnId) {
|
||||||
|
const contentMap = {};
|
||||||
|
for (const recip of recipients) {
|
||||||
|
if (!contentMap[recip.userId]) {
|
||||||
|
contentMap[recip.userId] = {};
|
||||||
|
}
|
||||||
|
contentMap[recip.userId][recip.deviceId] = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._baseApis.sendToDevice(
|
||||||
|
'm.room_key_request', contentMap, txnId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRequestBody(requestBody) {
|
||||||
|
// we assume that the request is for megolm keys, which are identified by
|
||||||
|
// room id and session id
|
||||||
|
return requestBody.room_id + " / " + requestBody.session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRecipientList(recipients) {
|
||||||
|
return '['
|
||||||
|
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||||
|
+ ']';
|
||||||
|
}
|
||||||
|
|
||||||
@@ -527,6 +527,14 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
|||||||
* problem decrypting the event
|
* problem decrypting the event
|
||||||
*/
|
*/
|
||||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||||
|
this._decryptEvent(event, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// helper for the real decryptEvent and for _retryDecryption. If
|
||||||
|
// requestKeysOnFail is true, we'll send an m.room_key_request when we fail
|
||||||
|
// to decrypt the event due to missing megolm keys.
|
||||||
|
MegolmDecryption.prototype._decryptEvent = function(event, requestKeysOnFail) {
|
||||||
const content = event.getWireContent();
|
const content = event.getWireContent();
|
||||||
|
|
||||||
if (!content.sender_key || !content.session_id ||
|
if (!content.sender_key || !content.session_id ||
|
||||||
@@ -543,6 +551,9 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||||
this._addEventToPendingList(event);
|
this._addEventToPendingList(event);
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
this._requestKeysForEvent(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new base.DecryptionError(
|
throw new base.DecryptionError(
|
||||||
e.toString(), {
|
e.toString(), {
|
||||||
@@ -554,6 +565,9 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
if (res === null) {
|
if (res === null) {
|
||||||
// We've got a message for a session we don't have.
|
// We've got a message for a session we don't have.
|
||||||
this._addEventToPendingList(event);
|
this._addEventToPendingList(event);
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
this._requestKeysForEvent(event);
|
||||||
|
}
|
||||||
throw new base.DecryptionError(
|
throw new base.DecryptionError(
|
||||||
"The sender's device has not sent us the keys for this message.",
|
"The sender's device has not sent us the keys for this message.",
|
||||||
{
|
{
|
||||||
@@ -576,6 +590,28 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
event.setClearData(payload, res.keysProved, res.keysClaimed);
|
event.setClearData(payload, res.keysProved, res.keysClaimed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
|
||||||
|
const sender = event.getSender();
|
||||||
|
const wireContent = event.getWireContent();
|
||||||
|
|
||||||
|
// send the request to all of our own devices, and the
|
||||||
|
// original sending device if it wasn't us.
|
||||||
|
const recipients = [{
|
||||||
|
userId: this._userId, deviceId: '*',
|
||||||
|
}];
|
||||||
|
if (sender != this._userId) {
|
||||||
|
recipients.push({
|
||||||
|
userId: sender, deviceId: wireContent.device_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._crypto.requestRoomKey({
|
||||||
|
room_id: event.getRoomId(),
|
||||||
|
algorithm: wireContent.algorithm,
|
||||||
|
sender_key: wireContent.sender_key,
|
||||||
|
session_id: wireContent.session_id,
|
||||||
|
}, recipients);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an event to the list of those we couldn't decrypt the first time we
|
* Add an event to the list of those we couldn't decrypt the first time we
|
||||||
@@ -657,7 +693,8 @@ MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
|
|||||||
|
|
||||||
for (let i = 0; i < pending.length; i++) {
|
for (let i = 0; i < pending.length; i++) {
|
||||||
try {
|
try {
|
||||||
this.decryptEvent(pending[i]);
|
// no point sending another m.room_key_request here.
|
||||||
|
this._decryptEvent(pending[i], false);
|
||||||
console.log("successful re-decryption of", pending[i]);
|
console.log("successful re-decryption of", pending[i]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Still can't decrypt", pending[i], e.stack || e);
|
console.log("Still can't decrypt", pending[i], e.stack || e);
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const DeviceInfo = require("./deviceinfo");
|
|||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
const DeviceList = require('./DeviceList').default;
|
const DeviceList = require('./DeviceList').default;
|
||||||
|
|
||||||
|
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptography bits
|
* Cryptography bits
|
||||||
*
|
*
|
||||||
@@ -93,6 +95,10 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
|
|||||||
|
|
||||||
this._globalBlacklistUnverifiedDevices = false;
|
this._globalBlacklistUnverifiedDevices = false;
|
||||||
|
|
||||||
|
this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
|
||||||
|
baseApis, this._deviceId, this._cryptoStore,
|
||||||
|
);
|
||||||
|
|
||||||
let myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
let myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||||
this._userId,
|
this._userId,
|
||||||
);
|
);
|
||||||
@@ -124,8 +130,10 @@ function _registerEventHandlers(crypto, eventEmitter) {
|
|||||||
try {
|
try {
|
||||||
if (syncState === "STOPPED") {
|
if (syncState === "STOPPED") {
|
||||||
crypto._clientRunning = false;
|
crypto._clientRunning = false;
|
||||||
|
crypto._outgoingRoomKeyRequestManager.stop();
|
||||||
} else if (syncState === "PREPARED") {
|
} else if (syncState === "PREPARED") {
|
||||||
crypto._clientRunning = true;
|
crypto._clientRunning = true;
|
||||||
|
crypto._outgoingRoomKeyRequestManager.start();
|
||||||
}
|
}
|
||||||
if (syncState === "SYNCING") {
|
if (syncState === "SYNCING") {
|
||||||
crypto._onSyncCompleted(data);
|
crypto._onSyncCompleted(data);
|
||||||
@@ -787,6 +795,23 @@ Crypto.prototype.userDeviceListChanged = function(userId) {
|
|||||||
// processing the sync.
|
// processing the sync.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request for some room keys, if we have not already done so
|
||||||
|
*
|
||||||
|
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||||
|
* @param {Array<{userId: string, deviceId: string}>} recipients
|
||||||
|
*/
|
||||||
|
Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
|
||||||
|
this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
|
||||||
|
requestBody, recipients,
|
||||||
|
).catch((e) => {
|
||||||
|
// this normally means we couldn't talk to the store
|
||||||
|
console.error(
|
||||||
|
'Error requesting key for event', e,
|
||||||
|
);
|
||||||
|
}).done();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle an m.room.encryption event
|
* handle an m.room.encryption event
|
||||||
*
|
*
|
||||||
@@ -1126,5 +1151,13 @@ Crypto.prototype._signObject = function(obj) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameters of a room key request. The details of the request may
|
||||||
|
* vary with the crypto algorithm, but the management and storage layers for
|
||||||
|
* outgoing requests expect it to have 'room_id' and 'session_id' properties.
|
||||||
|
*
|
||||||
|
* @typedef {Object} RoomKeyRequestBody
|
||||||
|
*/
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
module.exports = Crypto;
|
module.exports = Crypto;
|
||||||
|
|||||||
Reference in New Issue
Block a user