Skip to content

Commit

Permalink
Merge pull request #1640 from uhoreg/room-history-key-sharing2
Browse files Browse the repository at this point in the history
Add function to share megolm keys for historical messages.
uhoreg authored Mar 25, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents bf25cb6 + f92b620 commit 37fb21f
Showing 6 changed files with 277 additions and 32 deletions.
33 changes: 33 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -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
// =========
// Operations on groups that come down the sync stream (ie. ones the
22 changes: 22 additions & 0 deletions src/crypto/OlmDevice.js
Original file line number Diff line number Diff line change
@@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
IndexedDBCryptoStore.STORE_SHARED_HISTORY_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.sharedHistory) {
this._cryptoStore.addSharedHistoryInboundGroupSession(
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,
"shared_history": sessionData.sharedHistory || 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(),
"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
// =========

178 changes: 147 additions & 31 deletions src/crypto/algorithms/megolm.js
Original file line number Diff line number Diff line change
@@ -36,11 +36,27 @@ import {

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
* @constructor
*
* @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 {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
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
function OutboundSessionInfo(sessionId, sharedHistory = false) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
this.sharedHistory = sharedHistory;
}


@@ -183,6 +200,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 +210,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 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const prepareSession = async (oldSession) => {
session = oldSession;

const sharedHistory = isRoomSharedHistory(room);

// history visibility changed
if (session && sharedHistory !== session.sharedHistory) {
session = null;
}

// need to make a brand new session?
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
@@ -219,7 +244,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(sharedHistory);
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
@@ -250,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,
"org.matrix.msc3061.shared_history": sharedHistory,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
@@ -374,15 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
/**
* @private
*
* @param {boolean} sharedHistory
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = async function() {
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},
key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false,
{sharedHistory: sharedHistory},
);

// don't wait for it to complete
@@ -391,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
sessionId, key.key,
);

return new OutboundSessionInfo(sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory);
};

/**
@@ -672,14 +701,15 @@ 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,
"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}`);
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 +975,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,
@@ -1370,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}

const extraSessionData = {};
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = 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)
@@ -1573,14 +1607,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,
"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
*/
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(
session.room_id,
session.sender_key,
@@ -1602,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
@@ -1681,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(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(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
);
39 changes: 38 additions & 1 deletion src/crypto/store/indexeddb-crypto-store-backend.js
Original file line number Diff line number Diff line change
@@ -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;
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) {
let startTime;
let description;
@@ -834,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["userId", "deviceId"],
});
}
if (oldVersion < 10) {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
// Expand as needed.
}

25 changes: 25 additions & 0 deletions src/crypto/store/indexeddb-crypto-store.js
Original file line number Diff line number Diff line change
@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
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
* 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_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_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
12 changes: 12 additions & 0 deletions src/crypto/store/memory-crypto-store.js
Original file line number Diff line number Diff line change
@@ -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) {

0 comments on commit 37fb21f

Please sign in to comment.