From ceb162eb012032e872f0009eb955e3a1b318428f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Mar 2021 19:51:22 -0500 Subject: [PATCH 1/5] initial work on room history key sharing, take 2 --- src/client.js | 33 +++++ src/crypto/OlmDevice.js | 22 +++ src/crypto/algorithms/megolm.js | 133 +++++++++++++++--- .../store/indexeddb-crypto-store-backend.js | 35 ++++- src/crypto/store/indexeddb-crypto-store.js | 14 ++ 5 files changed, 220 insertions(+), 17 deletions(-) diff --git a/src/client.js b/src/client.js index 16c79895df0..2671404efb5 100644 --- a/src/client.js +++ b/src/client.js @@ -2291,6 +2291,39 @@ 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. + */ +MatrixClient.prototype.sendShareableKeys = async function(roomId, userIds) { + 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.sendShareableInboundSessions) { + await alg.sendShareableInboundSessions(devicesByUser); + } 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..495f0eaf022 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( 'readwrite', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS, ], (txn) => { /* if we already have this session, consider updating it */ this._getInboundGroupSession( @@ -1104,6 +1105,12 @@ OlmDevice.prototype.addInboundGroupSession = async function( this._cryptoStore.storeEndToEndInboundGroupSession( senderKey, sessionId, sessionData, txn, ); + + if (!existingSession && extraSessionData.shareable) { + this._cryptoStore.addShareableInboundGroupSession( + roomId, senderKey, sessionId, txn, + ); + } } finally { session.free(); } @@ -1383,6 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], "sender_claimed_ed25519_key": senderEd25519Key, + "shareable": sessionData.shareable || false, }; }, ); @@ -1415,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function( "session_key": session.export_session(messageIndex), "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], "first_known_index": session.first_known_index(), + "io.element.unstable.shareable": sessionData.shareable || false, }; }); }; +OlmDevice.prototype.getShareableInboundGroupSessions = async function(roomId) { + let result; + await this._cryptoStore.doTxn( + 'readonly', [ + IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS, + ], (txn) => { + result = this._cryptoStore.getShareableInboundGroupSessions(roomId, txn); + }, + logger.withPrefix("[getShareableInboundGroupSessionsForRoom]"), + ); + return result; +}; + // Utilities // ========= diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 7ea048d2b83..ca4059f9424 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -36,6 +36,20 @@ import { import {WITHHELD_MESSAGES} from '../OlmDevice'; +// determine whether the key can be shared with invitees +function isRoomKeyShareable(room) { + const visibilityEvent = room.currentState + .getStateEvents("m.room.history_visibility", ""); + // NOTE: if the room visibility is unset, it would normally default to + // "world_readable". + // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5) + // But we will be paranoid here, and treat it as a situation where the key + // should not be shareable + const visibility = visibilityEvent && visibilityEvent.getContent() && + visibilityEvent.getContent().history_visibility; + return ["world_readable", "shared"].includes(visibility); +} + /** * @private * @constructor @@ -50,12 +64,13 @@ import {WITHHELD_MESSAGES} from '../OlmDevice'; * devices with which we have shared the session key * userId -> {deviceId -> msgindex} */ -function OutboundSessionInfo(sessionId) { +function OutboundSessionInfo(sessionId, shareable = false) { this.sessionId = sessionId; this.useCount = 0; this.creationTime = new Date().getTime(); this.sharedWithDevices = {}; this.blockedDevicesNotified = {}; + this.shareable = shareable; } @@ -183,6 +198,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm); /** * @private * + * @param {module:models/room} room * @param {Object} devicesInRoom The devices in this room, indexed by user ID * @param {Object} blocked The devices that are blocked, indexed by user ID * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm @@ -192,7 +208,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm); * OutboundSessionInfo when setup is complete. */ MegolmEncryption.prototype._ensureOutboundSession = async function( - devicesInRoom, blocked, singleOlmCreationPhase, + room, devicesInRoom, blocked, singleOlmCreationPhase, ) { let session; @@ -204,6 +220,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( const prepareSession = async (oldSession) => { session = oldSession; + const shareable = isRoomKeyShareable(room); + + // history visibility changed + if (session && shareable !== session.shareable) { + session = null; + } + // need to make a brand new session? if (session && session.needsRotation(this._sessionRotationPeriodMsgs, this._sessionRotationPeriodMs) @@ -219,7 +242,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( if (!session) { logger.log(`Starting new megolm session for room ${this._roomId}`); - session = await this._prepareNewSession(); + session = await this._prepareNewSession(shareable); logger.log(`Started new megolm session ${session.sessionId} ` + `for room ${this._roomId}`); this._outboundSessions[session.sessionId] = session; @@ -374,15 +397,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( /** * @private * + * @param {boolean} shareable + * * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session */ -MegolmEncryption.prototype._prepareNewSession = async function() { +MegolmEncryption.prototype._prepareNewSession = async function(shareable) { const sessionId = this._olmDevice.createOutboundGroupSession(); const key = this._olmDevice.getOutboundGroupSessionKey(sessionId); await this._olmDevice.addInboundGroupSession( this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, - key.key, {ed25519: this._olmDevice.deviceEd25519Key}, + key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false, + {shareable: shareable}, ); // don't wait for it to complete @@ -391,7 +417,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() { sessionId, key.key, ); - return new OutboundSessionInfo(sessionId); + return new OutboundSessionInfo(sessionId, shareable); }; /** @@ -680,6 +706,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( sender_key: senderKey, sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + "io.element.unstable.shareable": key.shareable || false, }, }; @@ -901,7 +928,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) { } logger.debug(`Ensuring outbound session in ${this._roomId}`); - await this._ensureOutboundSession(devicesInRoom, blocked, true); + await this._ensureOutboundSession(room, devicesInRoom, blocked, true); logger.debug(`Ready to encrypt events for ${this._roomId}`); } catch (e) { @@ -945,7 +972,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont this._checkForUnknownDevices(devicesInRoom); } - const session = await this._ensureOutboundSession(devicesInRoom, blocked); + const session = await this._ensureOutboundSession(room, devicesInRoom, blocked); const payloadJson = { room_id: this._roomId, type: eventType, @@ -1573,14 +1600,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function( return { type: "m.forwarded_room_key", content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: roomId, - sender_key: senderKey, - sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, - session_id: sessionId, - session_key: key.key, - chain_index: key.chain_index, - forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + "algorithm": olmlib.MEGOLM_ALGORITHM, + "room_id": roomId, + "sender_key": senderKey, + "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, + "session_id": sessionId, + "session_key": key.key, + "chain_index": key.chain_index, + "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain, + "io.element.unstable.shareable": key.shareable || false, }, }; }; @@ -1681,6 +1709,79 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) return !this._pendingEvents[senderKey]; }; +MegolmDecryption.prototype.sendShareableInboundSessions = async function(devicesByUser) { + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, devicesByUser, + ); + + logger.log("sendShareableInboundSessions to users", Object.keys(devicesByUser)); + + const shareableSessions = await this._olmDevice.getShareableInboundGroupSessions( + this._roomId, + ); + logger.log("shareable sessions", shareableSessions); + for (const [senderKey, sessionId] of shareableSessions) { + const payload = this._buildKeyForwardingMessage( + this._roomId, senderKey, sessionId, + ); + + 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); + } +}; + registerAlgorithm( olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 6ecffe7be42..e9d45e11e0f 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -19,7 +19,7 @@ limitations under the License. import {logger} from '../../logger'; import * as utils from "../../utils"; -export const VERSION = 9; +export const VERSION = 10; /** * Implementation of a CryptoStore which is backed by an existing @@ -758,6 +758,34 @@ export class Backend { })); } + addShareableInboundGroupSession(roomId, senderKey, sessionId, txn) { + if (!txn) { + txn = this._db.transaction("shareable_inbound_group_sessions", "readwrite"); + } + const objectStore = txn.objectStore("shareable_inbound_group_sessions"); + const req = objectStore.get([roomId]); + req.onsuccess = () => { + const {sessions} = req.result || {sessions: []}; + sessions.push([senderKey, sessionId]); + objectStore.put({roomId, sessions}); + }; + } + + getShareableInboundGroupSessions(roomId, txn) { + if (!txn) { + txn = this._db.transaction("shareable_inbound_group_sessions", "readonly"); + } + const objectStore = txn.objectStore("shareable_inbound_group_sessions"); + const req = objectStore.get([roomId]); + return new Promise((resolve, reject) => { + req.onsuccess = () => { + const {sessions} = req.result || {sessions: []}; + resolve(sessions); + }; + req.onerror = reject; + }); + } + doTxn(mode, stores, func, log = logger) { const txnId = this._nextTxnId++; const startTime = Date.now(); @@ -827,6 +855,11 @@ export function upgradeDatabase(db, oldVersion) { keyPath: ["userId", "deviceId"], }); } + if (oldVersion < 10) { + db.createObjectStore("shareable_inbound_group_sessions", { + keyPath: ["roomId"], + }); + } // Expand as needed. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 50f3c26789d..c7856de7fed 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -582,6 +582,18 @@ export class IndexedDBCryptoStore { return this._backend.markSessionsNeedingBackup(sessions, txn); } + /* FIXME: jsdoc + */ + addShareableInboundGroupSession(roomId, senderKey, sessionId, txn) { + return this._backend.addShareableInboundGroupSession( + roomId, senderKey, sessionId, txn, + ); + } + + getShareableInboundGroupSessions(roomId, txn) { + return this._backend.getShareableInboundGroupSessions(roomId, txn); + } + /** * Perform a transaction on the crypto store. Any store methods * that require a transaction (txn) object to be passed in may @@ -614,6 +626,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; +IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS + = 'shareable_inbound_group_sessions'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; From 0bfcb5071dd2748f96b132a86f9a930dad6bb650 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Mar 2021 20:04:34 -0500 Subject: [PATCH 2/5] fix test, lint --- src/crypto/algorithms/megolm.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ca4059f9424..044339f606a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -38,8 +38,8 @@ import {WITHHELD_MESSAGES} from '../OlmDevice'; // determine whether the key can be shared with invitees function isRoomKeyShareable(room) { - const visibilityEvent = room.currentState - .getStateEvents("m.room.history_visibility", ""); + const visibilityEvent = room.currentState && + room.currentState.getStateEvents("m.room.history_visibility", ""); // NOTE: if the room visibility is unset, it would normally default to // "world_readable". // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5) @@ -55,6 +55,8 @@ function isRoomKeyShareable(room) { * @constructor * * @param {string} sessionId + * @param {boolean} shareable whether the session can be freely shared with other + * group members, according to the room history visibility settings * * @property {string} sessionId * @property {Number} useCount number of times this session has been used @@ -698,14 +700,14 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( 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, + "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, "io.element.unstable.shareable": key.shareable || false, }, }; From a489691151231ec9f1b2ad3d4c045ab97286c965 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 11 Mar 2021 17:30:05 -0500 Subject: [PATCH 3/5] various fixes --- src/crypto/algorithms/megolm.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 044339f606a..655bb01030e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -275,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( const payload = { type: "m.room_key", content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: this._roomId, - session_id: session.sessionId, - session_key: key.key, - chain_index: key.chain_index, + "algorithm": olmlib.MEGOLM_ALGORITHM, + "room_id": this._roomId, + "session_id": session.sessionId, + "session_key": key.key, + "chain_index": key.chain_index, + "io.element.unstable.shareable": shareable, }, }; const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions( @@ -1399,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { keysClaimed = event.getKeysClaimed(); } + const extraSessionData = {}; + if (content["io.element.unstable.shareable"]) { + extraSessionData.shareable = true; + } return this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, - exportFormat, + exportFormat, extraSessionData, ).then(() => { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId) @@ -1624,6 +1629,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function( * @param {string} [opts.source] where the key came from */ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { + const extraSessionData = {}; + if (opts.untrusted) { + extraSessionData.untrusted = true; + } + if (session["io.element.unstable.shareable"]) { + extraSessionData.shareable = true; + } return this._olmDevice.addInboundGroupSession( session.room_id, session.sender_key, @@ -1632,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { session.session_key, session.sender_claimed_keys, true, - opts.untrusted ? { untrusted: opts.untrusted } : {}, + extraSessionData, ).then(() => { if (opts.source !== "backup") { // don't wait for it to complete @@ -1723,7 +1735,7 @@ MegolmDecryption.prototype.sendShareableInboundSessions = async function(devices ); logger.log("shareable sessions", shareableSessions); for (const [senderKey, sessionId] of shareableSessions) { - const payload = this._buildKeyForwardingMessage( + const payload = await this._buildKeyForwardingMessage( this._roomId, senderKey, sessionId, ); From 1c191b2278f2a740c9ce1eae03e84bd6128ebbac Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 15 Mar 2021 22:49:43 -0400 Subject: [PATCH 4/5] use new terminology and field name from MSC --- src/client.js | 8 +-- src/crypto/OlmDevice.js | 18 +++--- src/crypto/algorithms/megolm.js | 57 ++++++++++--------- .../store/indexeddb-crypto-store-backend.js | 18 +++--- src/crypto/store/indexeddb-crypto-store.js | 12 ++-- 5 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/client.js b/src/client.js index 2671404efb5..65ca597eb5f 100644 --- a/src/client.js +++ b/src/client.js @@ -2292,13 +2292,13 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio }; /** - * Share the decryption keys with the given users for the given messages. + * Share shared-history decryption keys with the given users. * * @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. */ -MatrixClient.prototype.sendShareableKeys = async function(roomId, userIds) { +MatrixClient.prototype.sendSharedHistoryKeys = async function(roomId, userIds) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -2317,8 +2317,8 @@ MatrixClient.prototype.sendShareableKeys = async function(roomId, userIds) { } const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm); - if (alg.sendShareableInboundSessions) { - await alg.sendShareableInboundSessions(devicesByUser); + if (alg.sendSharedHistoryInboundSessions) { + await alg.sendSharedHistoryInboundSessions(devicesByUser); } else { logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm); } diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 495f0eaf022..df0733e6569 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1048,7 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( 'readwrite', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, - IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS, ], (txn) => { /* if we already have this session, consider updating it */ this._getInboundGroupSession( @@ -1106,8 +1106,8 @@ OlmDevice.prototype.addInboundGroupSession = async function( senderKey, sessionId, sessionData, txn, ); - if (!existingSession && extraSessionData.shareable) { - this._cryptoStore.addShareableInboundGroupSession( + if (!existingSession && extraSessionData.sharedHistory) { + this._cryptoStore.addSharedHistoryInboundGroupSession( roomId, senderKey, sessionId, txn, ); } @@ -1390,7 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], "sender_claimed_ed25519_key": senderEd25519Key, - "shareable": sessionData.shareable || false, + "shared_history": sessionData.sharedHistory || false, }; }, ); @@ -1423,20 +1423,20 @@ OlmDevice.prototype.exportInboundGroupSession = function( "session_key": session.export_session(messageIndex), "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], "first_known_index": session.first_known_index(), - "io.element.unstable.shareable": sessionData.shareable || false, + "org.matrix.msc3061.shared_history": sessionData.sharedHistory || false, }; }); }; -OlmDevice.prototype.getShareableInboundGroupSessions = async function(roomId) { +OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) { let result; await this._cryptoStore.doTxn( 'readonly', [ - IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS, ], (txn) => { - result = this._cryptoStore.getShareableInboundGroupSessions(roomId, txn); + result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn); }, - logger.withPrefix("[getShareableInboundGroupSessionsForRoom]"), + logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"), ); return result; }; diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 655bb01030e..0bf77cd295d 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -37,14 +37,14 @@ import { import {WITHHELD_MESSAGES} from '../OlmDevice'; // determine whether the key can be shared with invitees -function isRoomKeyShareable(room) { +function isRoomSharedHistory(room) { const visibilityEvent = room.currentState && room.currentState.getStateEvents("m.room.history_visibility", ""); // NOTE: if the room visibility is unset, it would normally default to // "world_readable". // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5) - // But we will be paranoid here, and treat it as a situation where the key - // should not be shareable + // But we will be paranoid here, and treat it as a situation where the room + // is not shared-history const visibility = visibilityEvent && visibilityEvent.getContent() && visibilityEvent.getContent().history_visibility; return ["world_readable", "shared"].includes(visibility); @@ -55,8 +55,8 @@ function isRoomKeyShareable(room) { * @constructor * * @param {string} sessionId - * @param {boolean} shareable whether the session can be freely shared with other - * group members, according to the room history visibility settings + * @param {boolean} sharedHistory whether the session can be freely shared with + * other group members, according to the room history visibility settings * * @property {string} sessionId * @property {Number} useCount number of times this session has been used @@ -66,13 +66,13 @@ function isRoomKeyShareable(room) { * devices with which we have shared the session key * userId -> {deviceId -> msgindex} */ -function OutboundSessionInfo(sessionId, shareable = false) { +function OutboundSessionInfo(sessionId, sharedHistory = false) { this.sessionId = sessionId; this.useCount = 0; this.creationTime = new Date().getTime(); this.sharedWithDevices = {}; this.blockedDevicesNotified = {}; - this.shareable = shareable; + this.sharedHistory = sharedHistory; } @@ -222,10 +222,10 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( const prepareSession = async (oldSession) => { session = oldSession; - const shareable = isRoomKeyShareable(room); + const sharedHistory = isRoomSharedHistory(room); // history visibility changed - if (session && shareable !== session.shareable) { + if (session && sharedHistory !== session.sharedHistory) { session = null; } @@ -244,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( if (!session) { logger.log(`Starting new megolm session for room ${this._roomId}`); - session = await this._prepareNewSession(shareable); + session = await this._prepareNewSession(sharedHistory); logger.log(`Started new megolm session ${session.sessionId} ` + `for room ${this._roomId}`); this._outboundSessions[session.sessionId] = session; @@ -280,7 +280,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( "session_id": session.sessionId, "session_key": key.key, "chain_index": key.chain_index, - "io.element.unstable.shareable": shareable, + "org.matrix.msc3061.shared_history": sharedHistory, }, }; const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions( @@ -400,18 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function( /** * @private * - * @param {boolean} shareable + * @param {boolean} sharedHistory * * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session */ -MegolmEncryption.prototype._prepareNewSession = async function(shareable) { +MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) { const sessionId = this._olmDevice.createOutboundGroupSession(); const key = this._olmDevice.getOutboundGroupSessionKey(sessionId); await this._olmDevice.addInboundGroupSession( this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false, - {shareable: shareable}, + {sharedHistory: sharedHistory}, ); // don't wait for it to complete @@ -420,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function(shareable) { sessionId, key.key, ); - return new OutboundSessionInfo(sessionId, shareable); + return new OutboundSessionInfo(sessionId, sharedHistory); }; /** @@ -709,7 +709,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( "sender_key": senderKey, "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain, - "io.element.unstable.shareable": key.shareable || false, + "org.matrix.msc3061.shared_history": key.shared_history || false, }, }; @@ -1401,8 +1401,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { } const extraSessionData = {}; - if (content["io.element.unstable.shareable"]) { - extraSessionData.shareable = true; + if (content["org.matrix.msc3061.shared_history"]) { + extraSessionData.sharedHistory = true; } return this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, @@ -1615,7 +1615,7 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function( "session_key": key.key, "chain_index": key.chain_index, "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain, - "io.element.unstable.shareable": key.shareable || false, + "org.matrix.msc3061.shared_history": key.shared_history || false, }, }; }; @@ -1633,8 +1633,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { if (opts.untrusted) { extraSessionData.untrusted = true; } - if (session["io.element.unstable.shareable"]) { - extraSessionData.shareable = true; + if (session["org.matrix.msc3061.shared_history"]) { + extraSessionData.sharedHistory = true; } return this._olmDevice.addInboundGroupSession( session.room_id, @@ -1723,18 +1723,19 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) return !this._pendingEvents[senderKey]; }; -MegolmDecryption.prototype.sendShareableInboundSessions = async function(devicesByUser) { +MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) { await olmlib.ensureOlmSessionsForDevices( this._olmDevice, this._baseApis, devicesByUser, ); - logger.log("sendShareableInboundSessions to users", Object.keys(devicesByUser)); + logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser)); - const shareableSessions = await this._olmDevice.getShareableInboundGroupSessions( - this._roomId, - ); - logger.log("shareable sessions", shareableSessions); - for (const [senderKey, sessionId] of shareableSessions) { + const sharedHistorySessions = + await this._olmDevice.getSharedHistoryInboundGroupSessions( + this._roomId, + ); + logger.log("shared-history sessions", sharedHistorySessions); + for (const [senderKey, sessionId] of sharedHistorySessions) { const payload = await this._buildKeyForwardingMessage( this._roomId, senderKey, sessionId, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index e9d45e11e0f..a3473f7c116 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -758,11 +758,13 @@ export class Backend { })); } - addShareableInboundGroupSession(roomId, senderKey, sessionId, txn) { + addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { if (!txn) { - txn = this._db.transaction("shareable_inbound_group_sessions", "readwrite"); + txn = this._db.transaction( + "shared_history_inbound_group_sessions", "readwrite", + ); } - const objectStore = txn.objectStore("shareable_inbound_group_sessions"); + const objectStore = txn.objectStore("shared_history_inbound_group_sessions"); const req = objectStore.get([roomId]); req.onsuccess = () => { const {sessions} = req.result || {sessions: []}; @@ -771,11 +773,13 @@ export class Backend { }; } - getShareableInboundGroupSessions(roomId, txn) { + getSharedHistoryInboundGroupSessions(roomId, txn) { if (!txn) { - txn = this._db.transaction("shareable_inbound_group_sessions", "readonly"); + txn = this._db.transaction( + "shared_history_inbound_group_sessions", "readonly", + ); } - const objectStore = txn.objectStore("shareable_inbound_group_sessions"); + const objectStore = txn.objectStore("shared_history_inbound_group_sessions"); const req = objectStore.get([roomId]); return new Promise((resolve, reject) => { req.onsuccess = () => { @@ -856,7 +860,7 @@ export function upgradeDatabase(db, oldVersion) { }); } if (oldVersion < 10) { - db.createObjectStore("shareable_inbound_group_sessions", { + db.createObjectStore("shared_history_inbound_group_sessions", { keyPath: ["roomId"], }); } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c7856de7fed..3fe53a6d77e 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -584,14 +584,14 @@ export class IndexedDBCryptoStore { /* FIXME: jsdoc */ - addShareableInboundGroupSession(roomId, senderKey, sessionId, txn) { - return this._backend.addShareableInboundGroupSession( + addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { + return this._backend.addSharedHistoryInboundGroupSession( roomId, senderKey, sessionId, txn, ); } - getShareableInboundGroupSessions(roomId, txn) { - return this._backend.getShareableInboundGroupSessions(roomId, txn); + getSharedHistoryInboundGroupSessions(roomId, txn) { + return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn); } /** @@ -626,8 +626,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; -IndexedDBCryptoStore.STORE_SHAREABLE_INBOUND_GROUP_SESSIONS - = 'shareable_inbound_group_sessions'; +IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS + = 'shared_history_inbound_group_sessions'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; From 63810186582bcbf94ff4426e6cc7414dfd8cbdfc Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 16 Mar 2021 13:52:05 -0400 Subject: [PATCH 5/5] add jsdoc and implementation for memory crypto store --- src/crypto/store/indexeddb-crypto-store.js | 15 +++++++++++++-- src/crypto/store/memory-crypto-store.js | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 3fe53a6d77e..4cfe3612867 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -582,14 +582,25 @@ export class IndexedDBCryptoStore { return this._backend.markSessionsNeedingBackup(sessions, txn); } - /* FIXME: jsdoc + /** + * Add a shared-history group session for a room. + * @param {string} roomId The room that the key belongs to + * @param {string} senderKey The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {*} txn An active transaction. See doTxn(). (optional) */ addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { - return this._backend.addSharedHistoryInboundGroupSession( + this._backend.addSharedHistoryInboundGroupSession( roomId, senderKey, sessionId, txn, ); } + /** + * Get the shared-history group session for a room. + * @param {string} roomId The room that the key belongs to + * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} Resolves to an array of [senderKey, sessionId] + */ getSharedHistoryInboundGroupSessions(roomId, txn) { return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 5170fb2c30c..ac4e7be0b28 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -51,6 +51,8 @@ export class MemoryCryptoStore { this._rooms = {}; // Set of {senderCurve25519Key+'/'+sessionId} this._sessionsNeedingBackup = {}; + // roomId -> array of [senderKey, sessionId] + this._sharedHistoryInboundGroupSessions = {}; } /** @@ -467,6 +469,16 @@ export class MemoryCryptoStore { return Promise.resolve(); } + addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) { + const sessions = this._sharedHistoryInboundGroupSessions[roomId] || []; + sessions.push([senderKey, sessionId]); + this._sharedHistoryInboundGroupSessions[roomId] = sessions; + } + + getSharedHistoryInboundGroupSessions(roomId) { + return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []); + } + // Session key backups doTxn(mode, stores, func) {