1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

Make encryption asynchronous

We're going to need to send out a load of messages to distribute the megolm
keys; as a first step, deal with the asynchronicity this will require.
This commit is contained in:
Richard van der Hoff
2016-08-17 18:18:20 +01:00
parent e0bd05a8c4
commit 32fa51818b
8 changed files with 105 additions and 63 deletions

View File

@@ -822,10 +822,18 @@ function _sendEvent(client, room, event, callback) {
// so that we can handle synchronous and asynchronous exceptions with the // so that we can handle synchronous and asynchronous exceptions with the
// same code path. // same code path.
return q().then(function() { return q().then(function() {
var encryptionPromise = null;
if (client._crypto) { if (client._crypto) {
client._crypto.encryptEventIfNeeded(event, room); encryptionPromise = client._crypto.encryptEventIfNeeded(event, room);
} }
if (encryptionPromise) {
_updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
encryptionPromise = encryptionPromise.then(function() {
_updatePendingEventStatus(room, event, EventStatus.SENDING);
});
}
return encryptionPromise;
}).then(function() {
var promise; var promise;
// this event may be queued // this event may be queued
if (client.scheduler) { if (client.scheduler) {
@@ -857,11 +865,15 @@ function _sendEvent(client, room, event, callback) {
// the request failed to send. // the request failed to send.
console.error("Error sending event", err.stack || err); console.error("Error sending event", err.stack || err);
try {
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT); _updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
if (callback) { if (callback) {
callback(err); callback(err);
} }
} catch (err2) {
console.error("Exception in error handler!", err2.stack || err);
}
throw err; throw err;
}); });
} }

View File

@@ -20,6 +20,7 @@ limitations under the License.
* *
* @module crypto-algorithms/base * @module crypto-algorithms/base
*/ */
var q = require("q");
var utils = require("../utils"); var utils = require("../utils");
@@ -43,6 +44,7 @@ module.exports.DECRYPTION_CLASSES = {};
* base type for encryption implementations * base type for encryption implementations
* *
* @constructor * @constructor
* @alias module:crypto-algorithms/base.EncryptionAlgorithm
* *
* @param {object} params parameters * @param {object} params parameters
* @param {string} params.deviceId The identifier for this device. * @param {string} params.deviceId The identifier for this device.
@@ -50,22 +52,28 @@ module.exports.DECRYPTION_CLASSES = {};
* @param {module:OlmDevice} params.olmDevice olm.js wrapper * @param {module:OlmDevice} params.olmDevice olm.js wrapper
* @param {string} params.roomId The ID of the room we will be sending to * @param {string} params.roomId The ID of the room we will be sending to
*/ */
module.exports.EncryptionAlgorithm = function(params) { var EncryptionAlgorithm = function(params) {
this._deviceId = params.deviceId; this._deviceId = params.deviceId;
this._crypto = params.crypto; this._crypto = params.crypto;
this._olmDevice = params.olmDevice; this._olmDevice = params.olmDevice;
this._roomId = params.roomId; this._roomId = params.roomId;
}; };
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/** /**
* Initialise this EncryptionAlgorithm instance for a particular room * Initialise this EncryptionAlgorithm instance for a particular room.
* *
* @method module:crypto-algorithms/base.EncryptionAlgorithm#initRoomEncryption * <p>This will be called once per EncryptionAlgorithm, just after the
* @abstract * constructor is called.
* *
* @param {string[]} roomMembers list of currently-joined users in the room * @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete * @return {module:client.Promise} Promise which resolves when setup is complete
*/ */
EncryptionAlgorithm.prototype.initRoomEncryption = function(roomMembers) {
return q();
};
/** /**
* Encrypt a message event * Encrypt a message event
@@ -77,7 +85,7 @@ module.exports.EncryptionAlgorithm = function(params) {
* @param {string} eventType * @param {string} eventType
* @param {object} plaintext event content * @param {object} plaintext event content
* *
* @return {object} new event body * @return {module:client.Promise} Promise which resolves to the new event body
*/ */

View File

@@ -39,35 +39,33 @@ var MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
*/ */
function MegolmEncryption(params) { function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params); base.EncryptionAlgorithm.call(this, params);
this._prepPromise = null;
this._outboundSessionId = null; this._outboundSessionId = null;
} }
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm); utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @inheritdoc
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
MegolmEncryption.prototype.initRoomEncryption = function(roomMembers) {
// nothing to do here.
return q();
};
/** /**
* @private * @private
*
* @return {module:client.Promise} Promise which resolves when setup is
* complete.
*/ */
MegolmEncryption.prototype._ensureOutboundSession = function() { MegolmEncryption.prototype._ensureOutboundSession = function() {
if (this._outboundSessionId) { if (this._prepPromise) {
return; // prep already in progress
return this._prepPromise;
} }
if (this._outboundSessionId) {
// prep already done
return q();
}
var session_id = this._olmDevice.createOutboundGroupSession(); var session_id = this._olmDevice.createOutboundGroupSession();
this._outboundSessionId = session_id; this._outboundSessionId = session_id;
var key = this._olmDevice.getOutboundGroupSessionKey(session_id); var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
// TODO: initiate key-sharing
console.log( console.log(
'Created outbound session. Add with window.mxMatrixClientPeg.' + 'Created outbound session. Add with window.mxMatrixClientPeg.' +
'matrixClient._crypto._olmDevice.addInboundGroupSession("' + 'matrixClient._crypto._olmDevice.addInboundGroupSession("' +
@@ -82,8 +80,17 @@ MegolmEncryption.prototype._ensureOutboundSession = function() {
this._roomId, this._olmDevice.deviceCurve25519Key, session_id, this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, key.chain_index key.key, key.chain_index
); );
var self = this;
// TODO: initiate key-sharing
this._prepPromise = q.delay(3000).then(function() {
console.log("woop woop, we totally shared the keys");
self._prepPromise = null;
});
return this._prepPromise;
}; };
/** /**
* @inheritdoc * @inheritdoc
* *
@@ -91,30 +98,31 @@ MegolmEncryption.prototype._ensureOutboundSession = function() {
* @param {string} eventType * @param {string} eventType
* @param {object} plaintext event content * @param {object} plaintext event content
* *
* @return {object} new event body * @return {module:client.Promise} Promise which resolves to the new event body
*/ */
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
this._ensureOutboundSession(); var self = this;
return this._ensureOutboundSession().then(function() {
var payloadJson = { var payloadJson = {
room_id: this._roomId, room_id: self._roomId,
type: eventType, type: eventType,
content: content content: content
}; };
var ciphertext = this._olmDevice.encryptGroupMessage( var ciphertext = self._olmDevice.encryptGroupMessage(
this._outboundSessionId, JSON.stringify(payloadJson) self._outboundSessionId, JSON.stringify(payloadJson)
); );
var encryptedContent = { var encryptedContent = {
algorithm: MEGOLM_ALGORITHM, algorithm: MEGOLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key, sender_key: self._olmDevice.deviceCurve25519Key,
body: ciphertext, body: ciphertext,
session_id: this._outboundSessionId, session_id: self._outboundSessionId,
signature: "FIXME", signature: "FIXME",
}; };
return encryptedContent; return encryptedContent;
});
}; };
/** /**

View File

@@ -20,6 +20,7 @@ limitations under the License.
* *
* @module crypto-algorithms/olm * @module crypto-algorithms/olm
*/ */
var q = require('q');
var utils = require("../utils"); var utils = require("../utils");
@@ -60,7 +61,7 @@ OlmEncryption.prototype.initRoomEncryption = function(roomMembers) {
* @param {string} eventType * @param {string} eventType
* @param {object} plaintext event content * @param {object} plaintext event content
* *
* @return {object} new event body * @return {module:client.Promise} Promise which resolves to the new event body
*/ */
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) { OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
if (!room) { if (!room) {
@@ -128,7 +129,7 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
sender_key: this._olmDevice.deviceCurve25519Key, sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext ciphertext: ciphertext
}; };
return encryptedContent; return q(encryptedContent);
}; };
/** /**

View File

@@ -600,7 +600,7 @@ Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
* @param {object} config The encryption config for the room. * @param {object} config The encryption config for the room.
* @param {string[]} roomMembers userIds of room members to start sessions with * @param {string[]} roomMembers userIds of room members to start sessions with
* *
* @return {Object} A promise that will resolve when encryption is setup. * @return {module:client.Promise} A promise that will resolve when encryption is setup.
*/ */
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) { Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
// if we already have encryption in this room, we should ignore this event // if we already have encryption in this room, we should ignore this event
@@ -727,18 +727,21 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
* *
* @param {module:models/room?} room destination room. Null if the destination * @param {module:models/room?} room destination room. Null if the destination
* is not a room we have seen over the sync pipe. * is not a room we have seen over the sync pipe.
*
* @return {module:client.Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/ */
Crypto.prototype.encryptEventIfNeeded = function(event, room) { Crypto.prototype.encryptEventIfNeeded = function(event, room) {
if (event.isEncrypted()) { if (event.isEncrypted()) {
// this event has already been encrypted; this happens if the // this event has already been encrypted; this happens if the
// encryption step succeeded, but the send step failed on the first // encryption step succeeded, but the send step failed on the first
// attempt. // attempt.
return; return null;
} }
if (event.getType() !== "m.room.message") { if (event.getType() !== "m.room.message") {
// we only encrypt m.room.message // we only encrypt m.room.message
return; return null;
} }
var roomId = event.getRoomId(); var roomId = event.getRoomId();
@@ -755,13 +758,14 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
"configuration event." "configuration event."
); );
} }
return; return null;
} }
var encryptedContent = alg.encryptMessage( return alg.encryptMessage(
room, event.getType(), event.getContent() room, event.getType(), event.getContent()
); ).then(function(encryptedContent) {
event.makeEncrypted("m.room.encrypted", encryptedContent); event.makeEncrypted("m.room.encrypted", encryptedContent);
});
}; };
/** /**

View File

@@ -29,6 +29,10 @@ limitations under the License.
module.exports.EventStatus = { module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */ /** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent", NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */ /** The event is in the process of being sent. */
SENDING: "sending", SENDING: "sending",
/** The event is in a queue waiting to be sent. */ /** The event is in a queue waiting to be sent. */

View File

@@ -845,8 +845,17 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
*/ */
var ALLOWED_TRANSITIONS = {}; var ALLOWED_TRANSITIONS = {};
ALLOWED_TRANSITIONS[EventStatus.SENDING] = ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [
[EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT]; EventStatus.SENDING,
EventStatus.NOT_SENT,
];
ALLOWED_TRANSITIONS[EventStatus.SENDING] = [
EventStatus.ENCRYPTING,
EventStatus.QUEUED,
EventStatus.NOT_SENT,
EventStatus.SENT,
];
ALLOWED_TRANSITIONS[EventStatus.QUEUED] = ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
[EventStatus.SENDING, EventStatus.CANCELLED]; [EventStatus.SENDING, EventStatus.CANCELLED];

View File

@@ -1,5 +1,4 @@
"use strict"; "use strict";
var q = require("q");
var sdk = require("../.."); var sdk = require("../..");
var HttpBackend = require("../mock-request"); var HttpBackend = require("../mock-request");
var utils = require("../test-utils"); var utils = require("../test-utils");
@@ -69,8 +68,8 @@ describe("MatrixClient retrying", function() {
expect(ev1.status).toEqual(EventStatus.SENDING); expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING); expect(ev2.status).toEqual(EventStatus.SENDING);
// give the reactor a chance to run, so that ev2 gets queued // the first message should get sent, and the second should get queued
q().then(function() { httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued // ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED); expect(ev2.status).toEqual(EventStatus.QUEUED);
@@ -82,12 +81,9 @@ describe("MatrixClient retrying", function() {
// shouldn't be able to cancel the first message yet // shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); }) expect(function() { client.cancelPendingEvent(ev1); })
.toThrow(); .toThrow();
}).respond(400); // fail the first message
// fail the first send httpBackend.flush().then(function() {
httpBackend.when("PUT", "/send/m.room.message/")
.respond(400);
return httpBackend.flush();
}).then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT); expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1); expect(tl.length).toEqual(1);