From 68b230a78fe6f458aed906ef19e6faaf685cdc6c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 8 Mar 2018 15:01:01 +0000 Subject: [PATCH] Add function to cancel and resend key request (#624) --- src/client.js | 10 +++ src/crypto/OutgoingRoomKeyRequestManager.js | 82 ++++++++++++++++----- src/crypto/index.js | 6 +- src/models/event.js | 15 ++++ 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/src/client.js b/src/client.js index 553f30462..fe2dff69e 100644 --- a/src/client.js +++ b/src/client.js @@ -623,6 +623,16 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) { return device.isVerified(); }; +/** + * Cancel a room key request for this event if one is ongoing and resend the + * request. + * @param {MatrxEvent} event event of which to cancel and resend the room + * key request. + */ +MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) { + event.cancelAndResendKeyRequest(this._crypto); +}; + /** * Enable end-to-end encryption for a room. * @param {string} roomId The room ID to enable encryption in. diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 8bc860d62..75ab35454 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -35,13 +35,19 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500; * * The state machine looks like: * - * | - * V (cancellation requested) - * UNSENT -----------------------------+ - * | | - * | (send successful) | - * V | - * SENT | + * | (cancellation sent) + * | .-------------------------------------------------. + * | | | + * V V (cancellation requested) | + * UNSENT -----------------------------+ | + * | | | + * | | | + * | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND + * V | Λ + * SENT | | + * |-------------------------------- | --------------' + * | | (cancellation requested with intent + * | | to resend the original request) * | | * | (cancellation requested) | * V | @@ -62,6 +68,12 @@ const ROOM_KEY_REQUEST_STATES = { /** reply received, cancellation not yet sent */ CANCELLATION_PENDING: 2, + + /** + * Cancellation not yet sent and will transition to UNSENT instead of + * being deleted once the cancellation has been sent. + */ + CANCELLATION_PENDING_AND_WILL_RESEND: 3, }; export default class OutgoingRoomKeyRequestManager { @@ -130,14 +142,16 @@ export default class OutgoingRoomKeyRequestManager { } /** - * Cancel room key requests, if any match the given details + * Cancel room key requests, if any match the given requestBody * * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {boolean} andResend if true, transition to UNSENT instead of + * deleting after sending cancellation. * * @returns {Promise} resolves when the request has been updated in our * pending list. */ - cancelRoomKeyRequest(requestBody) { + cancelRoomKeyRequest(requestBody, andResend=false) { return this._cryptoStore.getOutgoingRoomKeyRequest( requestBody, ).then((req) => { @@ -147,6 +161,7 @@ export default class OutgoingRoomKeyRequestManager { } switch (req.state) { case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: // nothing to do here return; @@ -166,11 +181,16 @@ export default class OutgoingRoomKeyRequestManager { req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT, ); - case ROOM_KEY_REQUEST_STATES.SENT: + case ROOM_KEY_REQUEST_STATES.SENT: { + // If `andResend` is set, transition to UNSENT once the + // cancellation has successfully been sent. + const state = andResend ? + ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND : + ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING; // send a cancellation. return this._cryptoStore.updateOutgoingRoomKeyRequest( req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { - state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, + state, cancellationTxnId: this._baseApis.makeTxnId(), }, ).then((updatedReq) => { @@ -200,15 +220,23 @@ export default class OutgoingRoomKeyRequestManager { // do.) this._sendOutgoingRoomKeyRequestCancellation( updatedReq, + andResend, ).catch((e) => { console.error( "Error sending room key request cancellation;" + " will retry later.", e, ); this._startTimer(); - }).done(); + }).then(() => { + if (!andResend) return; + // The request has transitioned from + // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We + // still need to resend the request which is now UNSENT, so + // start the timer if it isn't already started. + this._startTimer(); + }); }); - + } default: throw new Error('unhandled state: ' + req.state); } @@ -258,6 +286,7 @@ export default class OutgoingRoomKeyRequestManager { return this._cryptoStore.getOutgoingRoomKeyRequestByState([ ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, + ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, ROOM_KEY_REQUEST_STATES.UNSENT, ]).then((req) => { if (!req) { @@ -267,10 +296,16 @@ export default class OutgoingRoomKeyRequestManager { } let prom; - if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) { - prom = this._sendOutgoingRoomKeyRequest(req); - } else { // must be a cancellation - prom = this._sendOutgoingRoomKeyRequestCancellation(req); + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.UNSENT: + prom = this._sendOutgoingRoomKeyRequest(req); + break; + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + prom = this._sendOutgoingRoomKeyRequestCancellation(req); + break; + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + prom = this._sendOutgoingRoomKeyRequestCancellation(req, true); + break; } return prom.then(() => { @@ -309,8 +344,9 @@ export default class OutgoingRoomKeyRequestManager { }); } - // given a RoomKeyRequest, cancel it and delete the request record - _sendOutgoingRoomKeyRequestCancellation(req) { + // Given a RoomKeyRequest, cancel it and delete the request record unless + // andResend is set, in which case transition to UNSENT. + _sendOutgoingRoomKeyRequestCancellation(req, andResend) { console.log( `Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + @@ -327,6 +363,14 @@ export default class OutgoingRoomKeyRequestManager { return this._sendMessageToDevices( requestMessage, req.recipients, req.cancellationTxnId, ).then(() => { + if (andResend) { + // We want to resend, so transition to UNSENT + return this._cryptoStore.updateOutgoingRoomKeyRequest( + req.requestId, + ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, + { state: ROOM_KEY_REQUEST_STATES.UNSENT }, + ); + } return this._cryptoStore.deleteOutgoingRoomKeyRequest( req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, ); diff --git a/src/crypto/index.js b/src/crypto/index.js index d773285db..c83b590d8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -856,9 +856,11 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) { * * @param {module:crypto~RoomKeyRequestBody} requestBody * parameters to match for cancellation + * @param {boolean} andResend + * if true, resend the key request after cancelling. */ -Crypto.prototype.cancelRoomKeyRequest = function(requestBody) { - this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) +Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) { + this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend) .catch((e) => { console.warn("Error clearing pending room key requests", e); }).done(); diff --git a/src/models/event.js b/src/models/event.js index 26559c688..0bc2a3d40 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -378,6 +378,21 @@ utils.extend(module.exports.MatrixEvent.prototype, { return this._decryptionPromise; }, + /** + * Cancel any room key request for this event and resend another. + * + * @param {module:crypto} crypto crypto module + */ + cancelAndResendKeyRequest: function(crypto) { + const wireContent = this.getWireContent(); + crypto.cancelRoomKeyRequest({ + algorithm: wireContent.algorithm, + room_id: this.getRoomId(), + session_id: wireContent.session_id, + sender_key: wireContent.sender_key, + }, true); + }, + _decryptionLoop: async function(crypto) { // make sure that this method never runs completely synchronously. // (doing so would mean that we would clear _decryptionPromise *before*