From 6d88c764640610a0547b818e6b5ac98c0d109c82 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 30 May 2017 23:30:05 +0100 Subject: [PATCH 1/2] Storage layer for management of outgoing room key requests --- src/crypto/store/base.js | 23 ++ src/crypto/store/indexeddb-crypto-store.js | 250 +++++++++++++++++++++ src/crypto/store/memory-crypto-store.js | 142 ++++++++++++ 3 files changed, 415 insertions(+) diff --git a/src/crypto/store/base.js b/src/crypto/store/base.js index 1d26a1ab6f3..d9d1f7a94b0 100644 --- a/src/crypto/store/base.js +++ b/src/crypto/store/base.js @@ -9,3 +9,26 @@ * * @interface CryptoStore */ + +/** + * Represents an outgoing room key request + * + * @typedef {Object} OutgoingRoomKeyRequest + * + * @property {string} requestId unique id for this request. Used for both + * an id within the request for later pairing with a cancellation, and for + * the transaction id when sending the to_device messages to our local + * server. + * + * @property {string?} cancellationTxnId + * transaction id for the cancellation, if any + * + * @property {Array<{userId: string, deviceId: string}>} recipients + * list of recipients for the request + * + * @property {module:crypto~RoomKeyRequestBody} requestBody + * parameters for the request. + * + * @property {Number} state current state of this request (states are defined + * in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES}) + */ diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index d2a08abcab4..78b45be6a9e 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -15,6 +15,7 @@ limitations under the License. */ import q from 'q'; +import utils from '../../utils'; /** * Internal module. indexeddb storage for e2e. @@ -110,6 +111,248 @@ export default class IndexedDBCryptoStore { }; }); } + + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + getOrAddOutgoingRoomKeyRequest(request) { + const requestBody = request.requestBody; + + return this.connect().then((db) => { + const deferred = q.defer(); + const txn = db.transaction("outgoingRoomKeyRequests", "readwrite"); + txn.onerror = deferred.reject; + + // first see if we already have an entry for this request. + this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { + if (existing) { + // this entry matches the request - return it. + console.log( + `already have key request outstanding for ` + + `${requestBody.room_id} / ${requestBody.session_id}: ` + + `not sending another`, + ); + deferred.resolve(existing); + return; + } + + // we got to the end of the list without finding a match + // - add the new request. + console.log( + `enqueueing key request for ${requestBody.room_id} / ` + + requestBody.session_id, + ); + const store = txn.objectStore("outgoingRoomKeyRequests"); + store.add(request); + txn.onsuccess = () => { deferred.resolve(request); }; + }); + + return deferred.promise; + }); + } + + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + getOutgoingRoomKeyRequest(requestBody) { + return this.connect().then((db) => { + const deferred = q.defer(); + + const txn = db.transaction("outgoingRoomKeyRequests", "readonly"); + txn.onerror = deferred.reject; + + this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { + deferred.resolve(existing); + }); + return deferred.promise; + }); + } + + /** + * look for an existing room key request in the db + * + * @private + * @param {IDBTransaction} txn database transaction + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * @param {Function} callback function to call with the results of the + * search. Either passed a matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found. + */ + _getOutgoingRoomKeyRequest(txn, requestBody, callback) { + const store = txn.objectStore("outgoingRoomKeyRequests"); + + const idx = store.index("session"); + const cursorReq = idx.openCursor([ + requestBody.room_id, + requestBody.session_id, + ]); + + cursorReq.onsuccess = (ev) => { + const cursor = ev.target.result; + if(!cursor) { + // no match found + callback(null); + return; + } + + const existing = cursor.value; + + if (utils.deepCompare(existing.requestBody, requestBody)) { + // got a match + callback(existing); + return; + } + + // look at the next entry in the index + cursor.continue(); + }; + } + + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states. If there are multiple + * requests in those states, an arbitrary one is chosen. + */ + getOutgoingRoomKeyRequestByState(wantedStates) { + if (wantedStates.length === 0) { + return q(null); + } + + // this is a bit tortuous because we need to make sure we do the lookup + // in a single transaction, to avoid having a race with the insertion + // code. + + // index into the wantedStates array + let stateIndex = 0; + let result; + + function onsuccess(ev) { + const cursor = ev.target.result; + if (cursor) { + // got a match + result = cursor.value; + return; + } + + // try the next state in the list + stateIndex++; + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + const wantedState = wantedStates[stateIndex]; + const cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + + return this.connect().then((db) => { + const txn = db.transaction("outgoingRoomKeyRequests", "readonly"); + const store = txn.objectStore("outgoingRoomKeyRequests"); + + const wantedState = wantedStates[stateIndex]; + const cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + + return promiseifyTxn(txn).then(() => result); + }); + } + + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + let result = null; + + function onsuccess(ev) { + const cursor = ev.target.result; + if (!cursor) { + return; + } + const data = cursor.value; + if (data.state != expectedState) { + console.warn( + `Cannot update room key request from ${expectedState} ` + + `as it was already updated to ${data.state}`, + ); + return; + } + Object.assign(data, updates); + cursor.update(data); + result = data; + } + + return this.connect().then((db) => { + const txn = db.transaction("outgoingRoomKeyRequests", "readwrite"); + const cursorReq = txn.objectStore("outgoingRoomKeyRequests") + .openCursor(requestId); + cursorReq.onsuccess = onsuccess; + return promiseifyTxn(txn).then(() => result); + }); + } + + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + deleteOutgoingRoomKeyRequest(requestId, expectedState) { + return this.connect().then((db) => { + const txn = db.transaction("outgoingRoomKeyRequests", "readwrite"); + const cursorReq = txn.objectStore("outgoingRoomKeyRequests") + .openCursor(requestId); + cursorReq.onsuccess = (ev) => { + const cursor = ev.target.result; + if (!cursor) { + return; + } + const data = cursor.value; + if (data.state != expectedState) { + console.warn( + `Cannot delete room key request in state ${data.state} ` + + `(expected ${expectedState})`, + ); + return; + } + cursor.delete(); + }; + return promiseifyTxn(txn); + }); + } } function createDatabase(db) { @@ -124,3 +367,10 @@ function createDatabase(db) { outgoingRoomKeyRequestsStore.createIndex("state", "state"); } + +function promiseifyTxn(txn) { + return new q.Promise((resolve, reject) => { + txn.oncomplete = resolve; + txn.onerror = reject; + }); +} diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 23bc2fbcae4..3f8e32d7d8e 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -16,6 +16,8 @@ limitations under the License. import q from 'q'; +import utils from '../../utils'; + /** * Internal module. in-memory storage for e2e. * @@ -27,6 +29,7 @@ import q from 'q'; */ export default class MemoryCryptoStore { constructor() { + this._outgoingRoomKeyRequests = []; } /** @@ -37,4 +40,143 @@ export default class MemoryCryptoStore { deleteAllData() { return q(); } + + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + getOrAddOutgoingRoomKeyRequest(request) { + const requestBody = request.requestBody; + + // first see if we already have an entry for this request. + return this.getOutgoingRoomKeyRequest(requestBody).then((existing) => { + if (existing) { + // this entry matches the request - return it. + console.log( + `already have key request outstanding for ` + + `${requestBody.room_id} / ${requestBody.session_id}: ` + + `not sending another`, + ); + return existing; + } + + // we got to the end of the list without finding a match + // - add the new request. + console.log( + `enqueueing key request for ${requestBody.room_id} / ` + + requestBody.session_id, + ); + this._outgoingRoomKeyRequests.push(request); + return request; + }); + } + + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + getOutgoingRoomKeyRequest(requestBody) { + for (const existing of this._outgoingRoomKeyRequests) { + if (utils.deepCompare(existing.requestBody, requestBody)) { + return q(existing); + } + } + return q(null); + } + + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states + */ + getOutgoingRoomKeyRequestByState(wantedStates) { + for (const req of this._outgoingRoomKeyRequests) { + for (const state of wantedStates) { + if (req.state === state) { + return q(req); + } + } + } + return q(null); + } + + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + for (const req of this._outgoingRoomKeyRequests) { + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + console.warn( + `Cannot update room key request from ${expectedState} ` + + `as it was already updated to ${req.state}`, + ); + return q(null); + } + Object.assign(req, updates); + return q(req); + } + + return q(null); + } + + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + deleteOutgoingRoomKeyRequest(requestId, expectedState) { + for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) { + const req = this._outgoingRoomKeyRequests[i]; + + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + console.warn( + `Cannot delete room key request in state ${req.state} ` + + `(expected ${expectedState})`, + ); + return q(null); + } + + this._outgoingRoomKeyRequests.splice(i, 1); + return q(req); + } + + return q(null); + } } From ea2a04135f59312686423b989fd1ee02f7b2b3b7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 31 May 2017 07:24:08 +0100 Subject: [PATCH 2/2] 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. --- src/crypto/OutgoingRoomKeyRequestManager.js | 217 ++++++++++++++++++++ src/crypto/algorithms/megolm.js | 39 +++- src/crypto/index.js | 33 +++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/crypto/OutgoingRoomKeyRequestManager.js diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js new file mode 100644 index 00000000000..d6e3ddeea13 --- /dev/null +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -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(",") + + ']'; +} + diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index f7d7b770e1b..75595c70ba3 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -527,6 +527,14 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm); * problem decrypting the 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(); if (!content.sender_key || !content.session_id || @@ -543,6 +551,9 @@ MegolmDecryption.prototype.decryptEvent = function(event) { } catch (e) { if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { this._addEventToPendingList(event); + if (requestKeysOnFail) { + this._requestKeysForEvent(event); + } } throw new base.DecryptionError( e.toString(), { @@ -554,6 +565,9 @@ MegolmDecryption.prototype.decryptEvent = function(event) { if (res === null) { // We've got a message for a session we don't have. this._addEventToPendingList(event); + if (requestKeysOnFail) { + this._requestKeysForEvent(event); + } throw new base.DecryptionError( "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); }; +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 @@ -657,7 +693,8 @@ MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) { for (let i = 0; i < pending.length; i++) { 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]); } catch (e) { console.log("Still can't decrypt", pending[i], e.stack || e); diff --git a/src/crypto/index.js b/src/crypto/index.js index c404721a5a9..a12838b8904 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -31,6 +31,8 @@ const DeviceInfo = require("./deviceinfo"); const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; +import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; + /** * Cryptography bits * @@ -93,6 +95,10 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId, this._globalBlacklistUnverifiedDevices = false; + this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( + baseApis, this._deviceId, this._cryptoStore, + ); + let myDevices = this._sessionStore.getEndToEndDevicesForUser( this._userId, ); @@ -124,8 +130,10 @@ function _registerEventHandlers(crypto, eventEmitter) { try { if (syncState === "STOPPED") { crypto._clientRunning = false; + crypto._outgoingRoomKeyRequestManager.stop(); } else if (syncState === "PREPARED") { crypto._clientRunning = true; + crypto._outgoingRoomKeyRequestManager.start(); } if (syncState === "SYNCING") { crypto._onSyncCompleted(data); @@ -787,6 +795,23 @@ Crypto.prototype.userDeviceListChanged = function(userId) { // 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 * @@ -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;