diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 18f4e1343..7f86f86ff 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -60,7 +60,8 @@ describe("MegolmDecryption", function() { // we stub out the olm encryption bits mockOlmLib = {}; mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy(); - mockOlmLib.encryptMessageForDevice = expect.createSpy(); + mockOlmLib.encryptMessageForDevice = + expect.createSpy().andReturn(Promise.resolve()); megolmDecryption.olmlib = mockOlmLib; }); diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index ab6d4e79f..fc938c33f 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -587,7 +587,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const unsigned = deviceResult.unsigned || {}; try { - olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); + await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); } catch (e) { console.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 948f1f89b..0ba1e2dec 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -288,7 +288,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse return olmlib.ensureOlmSessionsForDevices( this._olmDevice, this._baseApis, devicesByUser, ).then(function(devicemap) { - let haveTargets = false; + const promises = []; for (const userId in devicesByUser) { if (!devicesByUser.hasOwnProperty(userId)) { @@ -328,31 +328,35 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse ciphertext: {}, }; - olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - self._userId, - self._deviceId, - self._olmDevice, - userId, - deviceInfo, - payload, - ); - if (!contentMap[userId]) { contentMap[userId] = {}; } contentMap[userId][deviceId] = encryptedContent; - haveTargets = true; + + promises.push( + olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + self._userId, + self._deviceId, + self._olmDevice, + userId, + deviceInfo, + payload, + ), + ); } } - if (!haveTargets) { + if (promises.length === 0) { + // no devices to send to return Promise.resolve(); } - // TODO: retries - return self._baseApis.sendToDevice("m.room.encrypted", contentMap); + return Promise.all(promises).then(() => { + // TODO: retries + return self._baseApis.sendToDevice("m.room.encrypted", contentMap); + }); }).then(function() { console.log(`Completed megolm keyshare in ${self._roomId}`); @@ -751,7 +755,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { // // ensureOlmSessionsForUsers has already done the logging, // so just skip it. - return; + return null; } console.log( @@ -770,7 +774,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { ciphertext: {}, }; - this.olmlib.encryptMessageForDevice( + return this.olmlib.encryptMessageForDevice( encryptedContent.ciphertext, this._userId, this._deviceId, @@ -778,16 +782,16 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { userId, deviceInfo, payload, - ); + ).then(() => { + const contentMap = { + [userId]: { + [deviceId]: encryptedContent, + }, + }; - const contentMap = { - [userId]: { - [deviceId]: encryptedContent, - }, - }; - - // TODO: retries - return this._baseApis.sendToDevice("m.room.encrypted", contentMap); + // TODO: retries + return this._baseApis.sendToDevice("m.room.encrypted", contentMap); + }); }).done(); }; diff --git a/src/crypto/algorithms/olm.js b/src/crypto/algorithms/olm.js index c28ad9de2..c27e519a7 100644 --- a/src/crypto/algorithms/olm.js +++ b/src/crypto/algorithms/olm.js @@ -107,6 +107,8 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) { ciphertext: {}, }; + const promises = []; + for (let i = 0; i < users.length; ++i) { const userId = users[i]; const devices = self._crypto.getStoredDevicesForUser(userId); @@ -123,15 +125,17 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) { continue; } - olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - self._userId, self._deviceId, self._olmDevice, - userId, deviceInfo, payloadFields, + promises.push( + olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + self._userId, self._deviceId, self._olmDevice, + userId, deviceInfo, payloadFields, + ), ); } } - return encryptedContent; + return Promise.all(promises).return(encryptedContent); }); }; diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 476aef57d..e7fd76e3e 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -48,8 +48,11 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; * @param {string} recipientUserId * @param {module:crypto/deviceinfo} recipientDevice * @param {object} payloadFields fields to include in the encrypted payload + * + * Returns a promise which resolves (to undefined) when the payload + * has been encrypted into `resultsObject` */ -module.exports.encryptMessageForDevice = function( +module.exports.encryptMessageForDevice = async function( resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields, @@ -118,7 +121,7 @@ module.exports.encryptMessageForDevice = function( * an Object mapping from userId to deviceId to * {@link module:crypto~OlmSessionResult} */ -module.exports.ensureOlmSessionsForDevices = function( +module.exports.ensureOlmSessionsForDevices = async function( olmDevice, baseApis, devicesByUser, ) { const devicesWithoutSession = [ @@ -148,7 +151,7 @@ module.exports.ensureOlmSessionsForDevices = function( } if (devicesWithoutSession.length === 0) { - return Promise.resolve(result); + return result; } // TODO: this has a race condition - if we try to send another message @@ -158,55 +161,60 @@ module.exports.ensureOlmSessionsForDevices = function( // That should eventually resolve itself, but it's poor form. const oneTimeKeyAlgorithm = "signed_curve25519"; - return baseApis.claimOneTimeKeys( + const res = await baseApis.claimOneTimeKeys( devicesWithoutSession, oneTimeKeyAlgorithm, - ).then(function(res) { - const otk_res = res.one_time_keys || {}; - for (const userId in devicesByUser) { - if (!devicesByUser.hasOwnProperty(userId)) { + ); + + const otk_res = res.one_time_keys || {}; + const promises = []; + for (const userId in devicesByUser) { + if (!devicesByUser.hasOwnProperty(userId)) { + continue; + } + const userRes = otk_res[userId] || {}; + const devices = devicesByUser[userId]; + for (let j = 0; j < devices.length; j++) { + const deviceInfo = devices[j]; + const deviceId = deviceInfo.deviceId; + if (result[userId][deviceId].sessionId) { + // we already have a result for this device continue; } - const userRes = otk_res[userId] || {}; - const devices = devicesByUser[userId]; - for (let j = 0; j < devices.length; j++) { - const deviceInfo = devices[j]; - const deviceId = deviceInfo.deviceId; - if (result[userId][deviceId].sessionId) { - // we already have a result for this device - continue; - } - const deviceRes = userRes[deviceId] || {}; - let oneTimeKey = null; - for (const keyId in deviceRes) { - if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { - oneTimeKey = deviceRes[keyId]; - } + const deviceRes = userRes[deviceId] || {}; + let oneTimeKey = null; + for (const keyId in deviceRes) { + if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { + oneTimeKey = deviceRes[keyId]; } - - if (!oneTimeKey) { - console.warn( - "No one-time keys (alg=" + oneTimeKeyAlgorithm + - ") for device " + userId + ":" + deviceId, - ); - continue; - } - - const sid = _verifyKeyAndStartSession( - olmDevice, oneTimeKey, userId, deviceInfo, - ); - result[userId][deviceId].sessionId = sid; } + + if (!oneTimeKey) { + console.warn( + "No one-time keys (alg=" + oneTimeKeyAlgorithm + + ") for device " + userId + ":" + deviceId, + ); + continue; + } + + promises.push( + _verifyKeyAndStartSession( + olmDevice, oneTimeKey, userId, deviceInfo, + ).then((sid) => { + result[userId][deviceId].sessionId = sid; + }), + ); } - return result; - }); + } + + await Promise.all(promises); + return result; }; - -function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) { +async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) { const deviceId = deviceInfo.deviceId; try { - _verifySignature( + await _verifySignature( olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint(), ); @@ -249,8 +257,11 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) { * @param {string} signingDeviceId ID of the device whose signature should be checked * * @param {string} signingKey base64-ed ed25519 public key + * + * Returns a promise which resolves (to undefined) if the the signature is good, + * or rejects with an Error if it is bad. */ -const _verifySignature = module.exports.verifySignature = function( +const _verifySignature = module.exports.verifySignature = async function( olmDevice, obj, signingUserId, signingDeviceId, signingKey, ) { const signKeyId = "ed25519:" + signingDeviceId;