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

initial work on room history key sharing, take 2

This commit is contained in:
Hubert Chathi
2021-03-10 19:51:22 -05:00
parent fb73ab6878
commit ceb162eb01
5 changed files with 220 additions and 17 deletions

View File

@@ -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 // 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_SHAREABLE_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.shareable) {
this._cryptoStore.addShareableInboundGroupSession(
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,
"shareable": sessionData.shareable || 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(),
"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 // Utilities
// ========= // =========

View File

@@ -36,6 +36,20 @@ import {
import {WITHHELD_MESSAGES} from '../OlmDevice'; 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 * @private
* @constructor * @constructor
@@ -50,12 +64,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, shareable = 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.shareable = shareable;
} }
@@ -183,6 +198,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 +208,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 +220,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const prepareSession = async (oldSession) => { const prepareSession = async (oldSession) => {
session = oldSession; session = oldSession;
const shareable = isRoomKeyShareable(room);
// history visibility changed
if (session && shareable !== session.shareable) {
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 +242,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(shareable);
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;
@@ -374,15 +397,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
/** /**
* @private * @private
* *
* @param {boolean} shareable
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/ */
MegolmEncryption.prototype._prepareNewSession = async function() { MegolmEncryption.prototype._prepareNewSession = async function(shareable) {
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,
{shareable: shareable},
); );
// don't wait for it to complete // don't wait for it to complete
@@ -391,7 +417,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
sessionId, key.key, 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_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,
"io.element.unstable.shareable": key.shareable || false,
}, },
}; };
@@ -901,7 +928,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 +972,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,
@@ -1573,14 +1600,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,
"io.element.unstable.shareable": key.shareable || false,
}, },
}; };
}; };
@@ -1681,6 +1709,79 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
return !this._pendingEvents[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( 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;
/** /**
* Implementation of a CryptoStore which is backed by an existing * 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) { doTxn(mode, stores, func, log = logger) {
const txnId = this._nextTxnId++; const txnId = this._nextTxnId++;
const startTime = Date.now(); const startTime = Date.now();
@@ -827,6 +855,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["userId", "deviceId"], keyPath: ["userId", "deviceId"],
}); });
} }
if (oldVersion < 10) {
db.createObjectStore("shareable_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -582,6 +582,18 @@ export class IndexedDBCryptoStore {
return this._backend.markSessionsNeedingBackup(sessions, txn); 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 * 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 +626,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_SHAREABLE_INBOUND_GROUP_SESSIONS
= 'shareable_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';