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

Merge pull request #1640 from uhoreg/room-history-key-sharing2

Add function to share megolm keys for historical messages.
This commit is contained in:
Hubert Chathi
2021-03-25 14:18:12 -04:00
committed by GitHub
6 changed files with 277 additions and 32 deletions

View File

@@ -2303,6 +2303,39 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
); );
}; };
/**
* 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.sendSharedHistoryKeys = 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.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
};
// Group ops // Group ops
// ========= // =========
// Operations on groups that come down the sync stream (ie. ones the // Operations on groups that come down the sync stream (ie. ones the

View File

@@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
'readwrite', [ 'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => { ], (txn) => {
/* if we already have this session, consider updating it */ /* if we already have this session, consider updating it */
this._getInboundGroupSession( this._getInboundGroupSession(
@@ -1104,6 +1105,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
this._cryptoStore.storeEndToEndInboundGroupSession( this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn, senderKey, sessionId, sessionData, txn,
); );
if (!existingSession && extraSessionData.sharedHistory) {
this._cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally { } finally {
session.free(); session.free();
} }
@@ -1383,6 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
"forwarding_curve25519_key_chain": "forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [], sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key, "sender_claimed_ed25519_key": senderEd25519Key,
"shared_history": sessionData.sharedHistory || false,
}; };
}, },
); );
@@ -1415,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
"session_key": session.export_session(messageIndex), "session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
"first_known_index": session.first_known_index(), "first_known_index": session.first_known_index(),
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
}; };
}); });
}; };
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
let result;
await this._cryptoStore.doTxn(
'readonly', [
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
},
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
);
return result;
};
// Utilities // Utilities
// ========= // =========

View File

@@ -36,11 +36,27 @@ import {
import {WITHHELD_MESSAGES} from '../OlmDevice'; import {WITHHELD_MESSAGES} from '../OlmDevice';
// determine whether the key can be shared with invitees
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 room
// is not shared-history
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
return ["world_readable", "shared"].includes(visibility);
}
/** /**
* @private * @private
* @constructor * @constructor
* *
* @param {string} sessionId * @param {string} sessionId
* @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 {string} sessionId
* @property {Number} useCount number of times this session has been used * @property {Number} useCount number of times this session has been used
@@ -50,12 +66,13 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
* devices with which we have shared the session key * devices with which we have shared the session key
* userId -> {deviceId -> msgindex} * userId -> {deviceId -> msgindex}
*/ */
function OutboundSessionInfo(sessionId) { function OutboundSessionInfo(sessionId, sharedHistory = false) {
this.sessionId = sessionId; this.sessionId = sessionId;
this.useCount = 0; this.useCount = 0;
this.creationTime = new Date().getTime(); this.creationTime = new Date().getTime();
this.sharedWithDevices = {}; this.sharedWithDevices = {};
this.blockedDevicesNotified = {}; this.blockedDevicesNotified = {};
this.sharedHistory = sharedHistory;
} }
@@ -183,6 +200,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
/** /**
* @private * @private
* *
* @param {module:models/room} room
* @param {Object} devicesInRoom The devices in this room, indexed by user ID * @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 {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
@@ -192,7 +210,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
* OutboundSessionInfo when setup is complete. * OutboundSessionInfo when setup is complete.
*/ */
MegolmEncryption.prototype._ensureOutboundSession = async function( MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked, singleOlmCreationPhase, room, devicesInRoom, blocked, singleOlmCreationPhase,
) { ) {
let session; let session;
@@ -204,6 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const prepareSession = async (oldSession) => { const prepareSession = async (oldSession) => {
session = oldSession; session = oldSession;
const sharedHistory = isRoomSharedHistory(room);
// history visibility changed
if (session && sharedHistory !== session.sharedHistory) {
session = null;
}
// need to make a brand new session? // need to make a brand new session?
if (session && session.needsRotation(this._sessionRotationPeriodMsgs, if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs) this._sessionRotationPeriodMs)
@@ -219,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
if (!session) { if (!session) {
logger.log(`Starting new megolm session for room ${this._roomId}`); logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession(); session = await this._prepareNewSession(sharedHistory);
logger.log(`Started new megolm session ${session.sessionId} ` + logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${this._roomId}`); `for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session; this._outboundSessions[session.sessionId] = session;
@@ -250,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const payload = { const payload = {
type: "m.room_key", type: "m.room_key",
content: { content: {
algorithm: olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId, "room_id": this._roomId,
session_id: session.sessionId, "session_id": session.sessionId,
session_key: key.key, "session_key": key.key,
chain_index: key.chain_index, "chain_index": key.chain_index,
"org.matrix.msc3061.shared_history": sharedHistory,
}, },
}; };
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions( const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
@@ -374,15 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
/** /**
* @private * @private
* *
* @param {boolean} sharedHistory
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/ */
MegolmEncryption.prototype._prepareNewSession = async function() { MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
const sessionId = this._olmDevice.createOutboundGroupSession(); const sessionId = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId); const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
await this._olmDevice.addInboundGroupSession( await this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
key.key, {ed25519: this._olmDevice.deviceEd25519Key}, key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false,
{sharedHistory: sharedHistory},
); );
// don't wait for it to complete // don't wait for it to complete
@@ -391,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
sessionId, key.key, sessionId, key.key,
); );
return new OutboundSessionInfo(sessionId); return new OutboundSessionInfo(sessionId, sharedHistory);
}; };
/** /**
@@ -672,14 +701,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
const payload = { const payload = {
type: "m.forwarded_room_key", type: "m.forwarded_room_key",
content: { content: {
algorithm: olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId, "room_id": this._roomId,
session_id: sessionId, "session_id": sessionId,
session_key: key.key, "session_key": key.key,
chain_index: key.chain_index, "chain_index": key.chain_index,
sender_key: senderKey, "sender_key": senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
}, },
}; };
@@ -901,7 +931,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
} }
logger.debug(`Ensuring outbound session in ${this._roomId}`); 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}`); logger.debug(`Ready to encrypt events for ${this._roomId}`);
} catch (e) { } catch (e) {
@@ -945,7 +975,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
this._checkForUnknownDevices(devicesInRoom); this._checkForUnknownDevices(devicesInRoom);
} }
const session = await this._ensureOutboundSession(devicesInRoom, blocked); const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
const payloadJson = { const payloadJson = {
room_id: this._roomId, room_id: this._roomId,
type: eventType, type: eventType,
@@ -1370,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed(); keysClaimed = event.getKeysClaimed();
} }
const extraSessionData = {};
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession( return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId, content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed, content.session_key, keysClaimed,
exportFormat, exportFormat, extraSessionData,
).then(() => { ).then(() => {
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId) this._retryDecryption(senderKey, sessionId)
@@ -1573,14 +1607,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
return { return {
type: "m.forwarded_room_key", type: "m.forwarded_room_key",
content: { content: {
algorithm: olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
room_id: roomId, "room_id": roomId,
sender_key: senderKey, "sender_key": senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
session_id: sessionId, "session_id": sessionId,
session_key: key.key, "session_key": key.key,
chain_index: key.chain_index, "chain_index": key.chain_index,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
}, },
}; };
}; };
@@ -1594,6 +1629,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @param {string} [opts.source] where the key came from * @param {string} [opts.source] where the key came from
*/ */
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) { MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
const extraSessionData = {};
if (opts.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession( return this._olmDevice.addInboundGroupSession(
session.room_id, session.room_id,
session.sender_key, session.sender_key,
@@ -1602,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
session.session_key, session.session_key,
session.sender_claimed_keys, session.sender_claimed_keys,
true, true,
opts.untrusted ? { untrusted: opts.untrusted } : {}, extraSessionData,
).then(() => { ).then(() => {
if (opts.source !== "backup") { if (opts.source !== "backup") {
// don't wait for it to complete // don't wait for it to complete
@@ -1681,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
return !this._pendingEvents[senderKey]; return !this._pendingEvents[senderKey];
}; };
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
);
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
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,
);
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( registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption, olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
); );

View File

@@ -19,7 +19,7 @@ limitations under the License.
import {logger} from '../../logger'; import {logger} from '../../logger';
import * as utils from "../../utils"; import * as utils from "../../utils";
export const VERSION = 9; export const VERSION = 10;
const PROFILE_TRANSACTIONS = false; const PROFILE_TRANSACTIONS = false;
/** /**
@@ -759,6 +759,38 @@ export class Backend {
})); }));
} }
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readwrite",
);
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
const {sessions} = req.result || {sessions: []};
sessions.push([senderKey, sessionId]);
objectStore.put({roomId, sessions});
};
}
getSharedHistoryInboundGroupSessions(roomId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readonly",
);
}
const objectStore = txn.objectStore("shared_history_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) { doTxn(mode, stores, func, log = logger) {
let startTime; let startTime;
let description; let description;
@@ -834,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["userId", "deviceId"], keyPath: ["userId", "deviceId"],
}); });
} }
if (oldVersion < 10) {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
return this._backend.markSessionsNeedingBackup(sessions, txn); return this._backend.markSessionsNeedingBackup(sessions, txn);
} }
/**
* 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) {
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);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may
@@ -614,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld'; = 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';

View File

@@ -51,6 +51,8 @@ export class MemoryCryptoStore {
this._rooms = {}; this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId} // Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {}; this._sessionsNeedingBackup = {};
// roomId -> array of [senderKey, sessionId]
this._sharedHistoryInboundGroupSessions = {};
} }
/** /**
@@ -467,6 +469,16 @@ export class MemoryCryptoStore {
return Promise.resolve(); 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 // Session key backups
doTxn(mode, stores, func) { doTxn(mode, stores, func) {