diff --git a/src/client.js b/src/client.js index 16c79895df0..2136a123d62 100644 --- a/src/client.js +++ b/src/client.js @@ -2291,6 +2291,42 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio ); }; +/** + * Share the decryption keys with the given users for the given messages. + * + * @param {string} roomId the room for which keys should be shared. + * @param {array} userIds a list of users to share with. The keys will be sent to + * all of the user's current devices. + * @param {function} nextMessage a function that returns the next Matrix message to + * to share keys for each time it is called. The function should return a + * {module:models/event.MatrixEvent}. + */ +MatrixClient.prototype.shareKeysForMessages = async function(roomId, userIds, nextMessage) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const roomEncryption = this._roomList.getRoomEncryption(roomId); + if (!roomEncryption) { + // unknown room, or unencrypted room + logger.error("Unknown room. Not sharing decryption keys"); + return; + } + + const deviceInfos = await this._crypto.downloadKeys(userIds); + const devicesByUser = {}; + for (const [userId, devices] of Object.entries(deviceInfos)) { + devicesByUser[userId] = Object.values(devices); + } + + const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm); + if (alg.shareKeysForMessages) { + await alg.shareKeysForMessages(devicesByUser, nextMessage); + } else { + logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm); + } +}; + // Group ops // ========= // Operations on groups that come down the sync stream (ie. ones the diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 0989d19f383..329e64973ab 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1271,6 +1271,7 @@ OlmDevice.prototype.decryptGroupMessage = async function( ); result = { result: plaintext, + message_index: res.message_index, keysClaimed: sessionData.keysClaimed || {}, senderKey: senderKey, forwardingCurve25519KeyChain: ( diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 7ea048d2b83..02a0400369a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -1681,6 +1681,140 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) return !this._pendingEvents[senderKey]; }; +/** + * Share the keys with the given users for the given messages. + * + * @param {object} devicesByUser a map of user to array of module:crypto/deviceinfo. + * @param {function} nextMessage a function that returns the next Matrix message to + * to share keys for each time it is called. The function should return a + * {module:models/event.MatrixEvent}. + */ +MegolmDecryption.prototype.shareKeysForMessages = async function(devicesByUser, nextMessage) { + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, devicesByUser, + ); + + logger.log("shareKeysForMessages to users", Object.keys(devicesByUser)); + + const shareSession = async (senderKey, sessionId, index) => { + logger.log("Sharing session", senderKey, sessionId, index); + const key = await this._olmDevice.getInboundGroupSessionKey( + this._roomId, senderKey, sessionId, index, + ); + + const payload = { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + }, + }; + + const promises = []; + const contentMap = {}; + for (const [userId, devices] of Object.entries(devicesByUser)) { + contentMap[userId] = {}; + for (const deviceInfo of devices) { + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + contentMap[userId][deviceInfo.deviceId] = encryptedContent; + promises.push( + olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + userId, + deviceInfo, + payload, + ), + ); + } + } + await Promise.all(promises); + + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) { + logger.log( + "No ciphertext for device " + + userId + ":" + deviceId + ": pruning", + ); + delete contentMap[userId][deviceId]; + } + } + // No devices left for that user? Strip that too. + if (Object.keys(contentMap[userId]).length === 0) { + logger.log("Pruned all devices for user " + userId); + delete contentMap[userId]; + } + } + + // Is there anything left? + if (Object.keys(contentMap).length === 0) { + logger.log("No users left to send to: aborting"); + return; + } + + await this._baseApis.sendToDevice("m.room.encrypted", contentMap); + }; + + const sessionBySenderKey = {}; + for (let message = await nextMessage(); message !== undefined; message = await nextMessage()) { + if (!message.isEncrypted() || + message.getWireContent().algorithm !== olmlib.MEGOLM_ALGORITHM) { + continue; + } + const content = message.getWireContent(); + const senderKey = content.sender_key; + const sessionId = content.session_id; + let res; + try { + res = await this._olmDevice.decryptGroupMessage( + message.getRoomId(), content.sender_key, content.session_id, content.ciphertext, + message.getId(), message.getTs(), + ); + } catch (e) { + continue; + } + const index = res.message_index; + if (!Number.isFinite(index)) { + continue; + } + if (senderKey in sessionBySenderKey) { + const [oldSessionId, oldIndex] = sessionBySenderKey[senderKey]; + if (oldSessionId === sessionId) { + if (oldIndex >= index) { + sessionBySenderKey[senderKey] = [sessionId, index]; + } + } else { + sessionBySenderKey[senderKey] = [sessionId, index]; + + await shareSession(senderKey, oldSessionId, oldIndex); + } + } else { + sessionBySenderKey[senderKey] = [sessionId, index]; + } + } + + for (const [senderKey, [sessionId, index]] of Object.entries(sessionBySenderKey)) { + await shareSession(senderKey, sessionId, index); + } +}; + registerAlgorithm( olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption, );