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
// =========
// Operations on groups that come down the sync stream (ie. ones the

View File

@@ -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
// =========

View File

@@ -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,
);

View File

@@ -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.
}

View File

@@ -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';