From 633c92eae865e957121e08de634aeee11a8b3992 Mon Sep 17 00:00:00 2001 From: RaindropsSys Date: Mon, 24 Apr 2023 14:03:36 +0200 Subject: Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated) --- .../lib/crypto/OutgoingRoomKeyRequestManager.js | 394 +++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/lib/crypto/OutgoingRoomKeyRequestManager.js (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/lib/crypto/OutgoingRoomKeyRequestManager.js') diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/lib/crypto/OutgoingRoomKeyRequestManager.js b/includes/external/matrix/node_modules/matrix-js-sdk/lib/crypto/OutgoingRoomKeyRequestManager.js new file mode 100644 index 0000000..2bd25ba --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/lib/crypto/OutgoingRoomKeyRequestManager.js @@ -0,0 +1,394 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RoomKeyRequestState = exports.OutgoingRoomKeyRequestManager = void 0; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _uuid = require("uuid"); +var _logger = require("../logger"); +var _event = require("../@types/event"); +var _utils = require("../utils"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +/** + * 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. + */ + +// 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 + * + * The state machine looks like: + * ``` + * + * | (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 | + * CANCELLATION_PENDING | + * | | + * | (cancellation sent) | + * V | + * (deleted) <---------------------------+ + * ``` + */ +let RoomKeyRequestState; +exports.RoomKeyRequestState = RoomKeyRequestState; +(function (RoomKeyRequestState) { + RoomKeyRequestState[RoomKeyRequestState["Unsent"] = 0] = "Unsent"; + RoomKeyRequestState[RoomKeyRequestState["Sent"] = 1] = "Sent"; + RoomKeyRequestState[RoomKeyRequestState["CancellationPending"] = 2] = "CancellationPending"; + RoomKeyRequestState[RoomKeyRequestState["CancellationPendingAndWillResend"] = 3] = "CancellationPendingAndWillResend"; +})(RoomKeyRequestState || (exports.RoomKeyRequestState = RoomKeyRequestState = {})); +class OutgoingRoomKeyRequestManager { + // handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null + // if the callback has been set, or if it is still running. + + // sanity check to ensure that we don't end up with two concurrent runs + // of sendOutgoingRoomKeyRequests + + constructor(baseApis, deviceId, cryptoStore) { + this.baseApis = baseApis; + this.deviceId = deviceId; + this.cryptoStore = cryptoStore; + (0, _defineProperty2.default)(this, "sendOutgoingRoomKeyRequestsTimer", void 0); + (0, _defineProperty2.default)(this, "sendOutgoingRoomKeyRequestsRunning", false); + (0, _defineProperty2.default)(this, "clientRunning", true); + } + + /** + * Called when the client is stopped. Stops any running background processes. + */ + stop() { + _logger.logger.log("stopping OutgoingRoomKeyRequestManager"); + // stop the timer on the next run + this.clientRunning = false; + } + + /** + * Send any requests that have been queued + */ + sendQueuedRequests() { + this.startTimer(); + } + + /** + * Queue up a room key request, if we haven't already queued or sent one. + * + * 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 resend - whether to resend the key request if there is + * already one + * + * @returns resolves when the request has been added to the + * pending list (or we have established that a similar request already + * exists) + */ + async queueRoomKeyRequest(requestBody, recipients, resend = false) { + const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody); + if (!req) { + await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({ + requestBody: requestBody, + recipients: recipients, + requestId: this.baseApis.makeTxnId(), + state: RoomKeyRequestState.Unsent + }); + } else { + switch (req.state) { + case RoomKeyRequestState.CancellationPendingAndWillResend: + case RoomKeyRequestState.Unsent: + // nothing to do here, since we're going to send a request anyways + return; + case RoomKeyRequestState.CancellationPending: + { + // existing request is about to be cancelled. If we want to + // resend, then change the state so that it resends after + // cancelling. Otherwise, just cancel the cancellation. + const state = resend ? RoomKeyRequestState.CancellationPendingAndWillResend : RoomKeyRequestState.Sent; + await this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPending, { + state, + cancellationTxnId: this.baseApis.makeTxnId() + }); + break; + } + case RoomKeyRequestState.Sent: + { + // a request has already been sent. If we don't want to + // resend, then do nothing. If we do want to, then cancel the + // existing request and send a new one. + if (resend) { + const state = RoomKeyRequestState.CancellationPendingAndWillResend; + const updatedReq = await this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, { + state, + cancellationTxnId: this.baseApis.makeTxnId(), + // need to use a new transaction ID so that + // the request gets sent + requestTxnId: this.baseApis.makeTxnId() + }); + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the request + // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have + // raced with another tab to mark the request cancelled. + // Try again, to make sure the request is resent. + return this.queueRoomKeyRequest(requestBody, recipients, resend); + } + + // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + try { + await this.sendOutgoingRoomKeyRequestCancellation(updatedReq, true); + } catch (e) { + _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); + } + // 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. + } + + break; + } + default: + throw new Error("unhandled state: " + req.state); + } + } + } + + /** + * Cancel room key requests, if any match the given requestBody + * + * + * @returns resolves when the request has been updated in our + * pending list. + */ + cancelRoomKeyRequest(requestBody) { + return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(req => { + if (!req) { + // no request was made for this key + return; + } + switch (req.state) { + case RoomKeyRequestState.CancellationPending: + case RoomKeyRequestState.CancellationPendingAndWillResend: + // nothing to do here + return; + case RoomKeyRequestState.Unsent: + // just delete it + + // FIXME: ghahah we may have attempted to send it, and + // not yet got a successful response. So the server + // may have seen it, so we still need to send a cancellation + // in that case :/ + + _logger.logger.log("deleting unnecessary room key request for " + stringifyRequestBody(requestBody)); + return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent); + case RoomKeyRequestState.Sent: + { + // send a cancellation. + return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, { + state: RoomKeyRequestState.CancellationPending, + cancellationTxnId: this.baseApis.makeTxnId() + }).then(updatedReq => { + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the + // request in state ROOM_KEY_REQUEST_STATES.SENT, + // so we must have raced with another tab to mark + // the request cancelled. There is no point in + // sending another cancellation since the other tab + // will do it. + _logger.logger.log("Tried to cancel room key request for " + stringifyRequestBody(requestBody) + " but it was already cancelled in another tab"); + return; + } + + // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + this.sendOutgoingRoomKeyRequestCancellation(updatedReq).catch(e => { + _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); + this.startTimer(); + }); + }); + } + default: + throw new Error("unhandled state: " + req.state); + } + }); + } + + /** + * Look for room key requests by target device and state + * + * @param userId - Target user ID + * @param deviceId - Target device ID + * + * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest} + */ + getOutgoingSentRoomKeyRequest(userId, deviceId) { + return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); + } + + /** + * Find anything in `sent` state, and kick it around the loop again. + * This is intended for situations where something substantial has changed, and we + * don't really expect the other end to even care about the cancellation. + * For example, after initialization or self-verification. + * @returns An array of `queueRoomKeyRequest` outputs. + */ + async cancelAndResendAllOutgoingRequests() { + const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); + return Promise.all(outgoings.map(({ + requestBody, + recipients + }) => this.queueRoomKeyRequest(requestBody, recipients, true))); + } + + // 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; + }).catch(e => { + // this should only happen if there is an indexeddb error, + // in which case we're a bit stuffed anyway. + _logger.logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`); + }); + }; + this.sendOutgoingRoomKeyRequestsTimer = 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). + async sendOutgoingRoomKeyRequests() { + if (!this.clientRunning) { + this.sendOutgoingRoomKeyRequestsTimer = undefined; + return; + } + const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.CancellationPending, RoomKeyRequestState.CancellationPendingAndWillResend, RoomKeyRequestState.Unsent]); + if (!req) { + this.sendOutgoingRoomKeyRequestsTimer = undefined; + return; + } + try { + switch (req.state) { + case RoomKeyRequestState.Unsent: + await this.sendOutgoingRoomKeyRequest(req); + break; + case RoomKeyRequestState.CancellationPending: + await this.sendOutgoingRoomKeyRequestCancellation(req); + break; + case RoomKeyRequestState.CancellationPendingAndWillResend: + await this.sendOutgoingRoomKeyRequestCancellation(req, true); + break; + } + + // go around the loop again + return this.sendOutgoingRoomKeyRequests(); + } catch (e) { + _logger.logger.error("Error sending room key request; will retry later.", e); + this.sendOutgoingRoomKeyRequestsTimer = undefined; + } + } + + // given a RoomKeyRequest, send it and update the request record + sendOutgoingRoomKeyRequest(req) { + _logger.logger.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.requestTxnId || req.requestId).then(() => { + return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent, { + state: RoomKeyRequestState.Sent + }); + }); + } + + // Given a RoomKeyRequest, cancel it and delete the request record unless + // andResend is set, in which case transition to UNSENT. + sendOutgoingRoomKeyRequestCancellation(req, andResend = false) { + _logger.logger.log(`Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + `(cancellation id ${req.cancellationTxnId})`); + const requestMessage = { + action: "request_cancellation", + requesting_device_id: this.deviceId, + request_id: req.requestId + }; + 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, RoomKeyRequestState.CancellationPendingAndWillResend, { + state: RoomKeyRequestState.Unsent + }); + } + return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPending); + }); + } + + // send a RoomKeyRequest to a list of recipients + sendMessageToDevices(message, recipients, txnId) { + const contentMap = new _utils.MapWithDefault(() => new Map()); + for (const recip of recipients) { + const userDeviceMap = contentMap.getOrCreate(recip.userId); + userDeviceMap.set(recip.deviceId, _objectSpread(_objectSpread({}, message), {}, { + [_event.ToDeviceMessageId]: (0, _uuid.v4)() + })); + } + return this.baseApis.sendToDevice(_event.EventType.RoomKeyRequest, contentMap, txnId); + } +} +exports.OutgoingRoomKeyRequestManager = OutgoingRoomKeyRequestManager; +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 `[${recipients.map(r => `${r.userId}:${r.deviceId}`).join(",")}]`; +} +//# sourceMappingURL=OutgoingRoomKeyRequestManager.js.map \ No newline at end of file -- cgit