diff --git a/src/client.js b/src/client.js index abd65eb9d43..cb3b16951a6 100644 --- a/src/client.js +++ b/src/client.js @@ -40,6 +40,8 @@ const SyncApi = require("./sync"); const MatrixBaseApis = require("./base-apis"); const MatrixError = httpApi.MatrixError; +import reEmit from './reemit'; + const SCROLLBACK_DELAY_MS = 3000; let CRYPTO_ENABLED = false; @@ -166,6 +168,10 @@ function MatrixClient(opts) { this.store, opts.cryptoStore, ); + reEmit(this, this._crypto, [ + "crypto.roomKeyRequest", + "crypto.roomKeyRequestCancellation", + ]); this.olmVersion = Crypto.getOlmVersion(); } diff --git a/src/crypto/algorithms/base.js b/src/crypto/algorithms/base.js index 0f662f931a9..4ae90695c3c 100644 --- a/src/crypto/algorithms/base.js +++ b/src/crypto/algorithms/base.js @@ -140,6 +140,26 @@ class DecryptionAlgorithm { importRoomKey(session) { // ignore by default } + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param {module:crypto#RoomKeyRequest} keyRequest + * @return {boolean} true if we have the keys and could (theoretically) share + * them; else false. + */ + hasKeysForKeyRequest(keyRequest) { + return false; + } + + /** + * Send the response to a room key request + * + * @param {module:crypto#RoomKeyRequest} keyRequest + */ + shareKeysWithDevice(keyRequest) { + throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); + } } export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272 diff --git a/src/crypto/index.js b/src/crypto/index.js index a12838b8904..ed5a2906437 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -22,6 +22,7 @@ limitations under the License. const anotherjson = require('another-json'); const q = require("q"); +import {EventEmitter} from 'events'; const utils = require("../utils"); const OlmDevice = require("./OlmDevice"); @@ -99,6 +100,11 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId, baseApis, this._deviceId, this._cryptoStore, ); + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + this._receivedRoomKeyRequests = []; + this._receivedRoomKeyRequestCancellations = []; + let myDevices = this._sessionStore.getEndToEndDevicesForUser( this._userId, ); @@ -124,6 +130,8 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId, _registerEventHandlers(this, eventEmitter); } +utils.inherits(Crypto, EventEmitter); + function _registerEventHandlers(crypto, eventEmitter) { eventEmitter.on("sync", function(syncState, oldState, data) { @@ -157,6 +165,8 @@ function _registerEventHandlers(crypto, eventEmitter) { crypto._onRoomKeyEvent(event); } else if (event.getType() == "m.new_device") { crypto._onNewDeviceEvent(event); + } else if (event.getType() == "m.room_key_request") { + crypto._onRoomKeyRequestEvent(event); } } catch (e) { console.error("Error handling toDeviceEvent:", e); @@ -893,6 +903,7 @@ Crypto.prototype._onSyncCompleted = function(syncData) { // (https://github.com/vector-im/riot-web/issues/2782). if (!syncData.catchingUp) { _maybeUploadOneTimeKeys(this); + this._processReceivedRoomKeyRequests(); } }; @@ -1081,6 +1092,106 @@ Crypto.prototype._onNewDeviceEvent = function(event) { this._deviceList.invalidateUserDeviceList(userId); }; +/** + * Called when we get an m.room_key_request event. + * + * @private + * @param {module:models/event.MatrixEvent} event key request event + */ +Crypto.prototype._onRoomKeyRequestEvent = function(event) { + const content = event.getContent(); + if (content.action === "request") { + // Queue it up for now, because they tend to arrive before the room state + // events at initial sync, and we want to see if we know anything about the + // room before passing them on to the app. + const req = new IncomingRoomKeyRequest(event); + this._receivedRoomKeyRequests.push(req); + } else if (content.action === "request_cancellation") { + const req = new IncomingRoomKeyRequestCancellation(event); + this._receivedRoomKeyRequestCancellations.push(req); + } +}; + +/** + * Process any m.room_key_request events which were queued up during the + * current sync. + * + * @private + */ +Crypto.prototype._processReceivedRoomKeyRequests = function() { + const requests = this._receivedRoomKeyRequests; + this._receivedRoomKeyRequests = []; + for (const req of requests) { + const userId = req.userId; + const deviceId = req.deviceId; + + const body = req.requestBody; + const roomId = body.room_id; + const alg = body.algorithm; + + console.log(`m.room_key_request from ${userId}:${deviceId}` + + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); + + if (userId !== this._userId) { + // TODO: determine if we sent this device the keys already: in + // which case we can do so again. + console.log("Ignoring room key request from other user for now"); + return; + } + + // todo: should we queue up requests we don't yet have keys for, + // in case they turn up later? + + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + if (!this._roomDecryptors[roomId]) { + console.log(`room key request for unencrypted room ${roomId}`); + continue; + } + + const decryptor = this._roomDecryptors[roomId][alg]; + if (!decryptor) { + console.log(`room key request for unknown alg ${alg} in room ${roomId}`); + continue; + } + + if (!decryptor.hasKeysForKeyRequest(req)) { + console.log( + `room key request for unknown session ${roomId} / ` + + body.session_id, + ); + continue; + } + + req.share = () => { + decryptor.shareKeysWithDevice(req); + }; + + // if the device is is verified already, share the keys + const device = this._deviceList.getStoredDevice(userId, deviceId); + if (device && device.isVerified()) { + console.log('device is already verified: sharing keys'); + req.share(); + return; + } + + this.emit("crypto.roomKeyRequest", req); + } + + const cancellations = this._receivedRoomKeyRequestCancellations; + this._receivedRoomKeyRequestCancellations = []; + for (const cancellation of cancellations) { + console.log( + `m.room_key_request cancellation for ${cancellation.userId}:` + + `${cancellation.deviceId} (id ${cancellation.requestId})`, + ); + + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + this.emit("crypto.roomKeyRequestCancellation", cancellation); + } +}; /** * Get a decryptor for a given room and algorithm. @@ -1159,5 +1270,61 @@ Crypto.prototype._signObject = function(obj) { * @typedef {Object} RoomKeyRequestBody */ +/** + * Represents a received m.room_key_request event + * + * @property {string} userId user requesting the key + * @property {string} deviceId device requesting the key + * @property {string} requestId unique id for the request + * @property {RoomKeyRequestBody} requestBody + * @property {Function} share callback which, when called, will ask + * the relevant crypto algorithm implementation to share the keys for + * this request. + */ +class IncomingRoomKeyRequest { + constructor(event) { + const content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + this.requestBody = content.body || {}; + this.share = () => { + throw new Error("don't know how to share keys for this request yet"); + }; + } +} + +/** + * Represents a received m.room_key_request cancellation + * + * @property {string} userId user requesting the cancellation + * @property {string} deviceId device requesting the cancellation + * @property {string} requestId unique id for the request to be cancelled + */ +class IncomingRoomKeyRequestCancellation { + constructor(event) { + const content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + } +} + +/** + * Fires when we receive a room key request + * + * @event module:client~MatrixClient#"crypto.roomKeyRequest" + * @param {module:crypto~IncomingRoomKeyRequest} req request details + */ + +/** + * Fires when we receive a room key request cancellation + * + * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" + * @param {module:crypto~IncomingRoomKeyRequestCancellation} req + */ + /** */ module.exports = Crypto;