Merge pull request #265 from vector-im/bwindels/fix-201

Send withheld message when we can't create an olm session with a given device
This commit is contained in:
Bruno Windels 2021-03-08 17:22:08 +00:00 committed by GitHub
commit 2a106c5053
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 13 deletions

View file

@ -26,6 +26,7 @@ const ENCRYPTED_TYPE = "m.room.encrypted";
// note that encrypt could still create a new session // note that encrypt could still create a new session
const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min
// TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap
export class RoomEncryption { export class RoomEncryption {
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) { constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) {
this._room = room; this._room = room;
@ -349,36 +350,63 @@ export class RoomEncryption {
async _processShareRoomKeyOperation(operation, hsApi, log) { async _processShareRoomKeyOperation(operation, hsApi, log) {
log.set("id", operation.id); log.set("id", operation.id);
await this._deviceTracker.trackRoom(this._room, log); await this._deviceTracker.trackRoom(this._room, log);
let devices; let devices;
if (operation.userIds === null) { if (operation.userIds === null) {
devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log);
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
operation.userIds = userIds; operation.userIds = userIds;
const userIdsTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); await this._updateOperationsStore(operations => operations.update(operation));
try {
userIdsTxn.operations.update(operation);
} catch (err) {
userIdsTxn.abort();
throw err;
}
await userIdsTxn.complete();
} else { } else {
devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log); devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log);
} }
const messages = await log.wrap("olm encrypt", log => this._olmEncryption.encrypt( const messages = await log.wrap("olm encrypt", log => this._olmEncryption.encrypt(
"m.room_key", operation.roomKeyMessage, devices, hsApi, log)); "m.room_key", operation.roomKeyMessage, devices, hsApi, log));
const missingDevices = devices.filter(d => !messages.some(m => m.device === d));
await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, hsApi, log)); await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, hsApi, log));
if (missingDevices.length) {
await log.wrap("missingDevices", async log => {
log.set("devices", missingDevices.map(d => d.deviceId));
const unsentUserIds = operation.userIds.filter(userId => missingDevices.some(d => d.userId === userId));
log.set("unsentUserIds", unsentUserIds);
operation.userIds = unsentUserIds;
// first remove the users that we've sent the keys already from the operation,
// so if anything fails, we don't send them again
await this._updateOperationsStore(operations => operations.update(operation));
// now, let the devices we could not claim their key
const withheldMessage = this._megolmEncryption.createWithheldMessage(operation.roomKeyMessage, "m.no_olm", "OTKs exhausted");
await this._sendSharedMessageToDevices("org.matrix.room_key.withheld", withheldMessage, missingDevices, hsApi, log);
});
}
await this._updateOperationsStore(operations => operations.remove(operation.id));
}
const removeTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); async _updateOperationsStore(callback) {
const writeTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
try { try {
removeTxn.operations.remove(operation.id); callback(writeTxn.operations);
} catch (err) { } catch (err) {
removeTxn.abort(); writeTxn.abort();
throw err; throw err;
} }
await removeTxn.complete(); await writeTxn.complete();
}
async _sendSharedMessageToDevices(type, message, devices, hsApi, log) {
const devicesByUser = groupBy(devices, device => device.userId);
const payload = {
messages: Array.from(devicesByUser.entries()).reduce((userMap, [userId, devices]) => {
userMap[userId] = devices.reduce((deviceMap, device) => {
deviceMap[device.deviceId] = message;
return deviceMap;
}, {});
return userMap;
}, {})
};
const txnId = makeTxnId();
await hsApi.sendToDevice(type, payload, txnId, {log}).response();
} }
async _sendMessagesToDevices(type, messages, hsApi, log) { async _sendMessagesToDevices(type, messages, hsApi, log) {

View file

@ -43,6 +43,17 @@ export class Encryption {
} }
} }
createWithheldMessage(roomMessage, code, reason) {
return {
algorithm: roomMessage.algorithm,
code,
reason,
room_id: roomMessage.room_id,
sender_key: this._account.identityKeys.curve25519,
session_id: roomMessage.session_id
};
}
async ensureOutboundSession(roomId, encryptionParams) { async ensureOutboundSession(roomId, encryptionParams) {
let session = new this._olm.OutboundGroupSession(); let session = new this._olm.OutboundGroupSession();
try { try {

View file

@ -157,7 +157,7 @@ export class Encryption {
const {device, oneTimeKey} = target; const {device, oneTimeKey} = target;
target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey);
} }
this._storeSessions(newEncryptionTargets, timestamp); await this._storeSessions(newEncryptionTargets, timestamp);
} catch (err) { } catch (err) {
for (const target of newEncryptionTargets) { for (const target of newEncryptionTargets) {
target.dispose(); target.dispose();