You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
notify devices when we don't send them keys (#1135)
and handle incoming notifications
This commit is contained in:
@@ -617,6 +617,9 @@ describe("megolm", function() {
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
@@ -714,6 +717,9 @@ describe("megolm", function() {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
|
||||
@@ -8,6 +8,9 @@ import testUtils from '../../../test-utils';
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../../lib/crypto';
|
||||
import logger from '../../../../lib/logger';
|
||||
import TestClient from '../../../TestClient';
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
import Room from '../../../../lib/models/room';
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
@@ -342,4 +345,154 @@ describe("MegolmDecryption", function() {
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient1 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice1",
|
||||
)).client;
|
||||
const bobClient2 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice2",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [{userId: "@bob:example.com"}];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES = {
|
||||
bobdevice1: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice1",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
|
||||
},
|
||||
verified: 0,
|
||||
},
|
||||
bobdevice2: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
|
||||
},
|
||||
verified: -1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice1: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.unverified',
|
||||
reason:
|
||||
'The sender has disabled encrypting to unverified devices.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.blacklisted',
|
||||
reason: 'The sender has blocked you.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient1.stopClient();
|
||||
bobClient2.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.blacklisted",
|
||||
reason: "You have been blocked",
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2019 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +19,8 @@ limitations under the License.
|
||||
import logger from '../logger';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
import algorithms from './algorithms';
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
|
||||
@@ -818,9 +821,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, txn, func,
|
||||
) {
|
||||
this._cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData) => {
|
||||
senderKey, sessionId, txn, (sessionData, withheld) => {
|
||||
if (sessionData === null) {
|
||||
func(null);
|
||||
func(null, null, withheld);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -834,7 +837,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
}
|
||||
|
||||
this._unpickleInboundGroupSession(sessionData, (session) => {
|
||||
func(session, sessionData);
|
||||
func(session, sessionData, withheld);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -859,7 +862,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
exportFormat,
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
@@ -914,6 +920,60 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Record in the data store why an inbound group session was withheld.
|
||||
*
|
||||
* @param {string} roomId room that the session belongs to
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {string} code reason code
|
||||
* @param {string} reason human-readable version of `code`
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSessionWithheld = async function(
|
||||
roomId, senderKey, sessionId, code, reason,
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderKey, sessionId,
|
||||
{
|
||||
room_id: roomId,
|
||||
code: code,
|
||||
reason: reason,
|
||||
},
|
||||
txn,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const WITHHELD_MESSAGES = {
|
||||
"m.unverified": "The sender has disabled encrypting to unverified devices.",
|
||||
"m.blacklisted": "The sender has blocked you.",
|
||||
"m.unauthorised": "You are not authorised to read the message.",
|
||||
"m.no_olm": "Unable to establish a secure channel.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the message to use for the exception when a session key is withheld.
|
||||
*
|
||||
* @param {object} withheld An object that describes why the key was withheld.
|
||||
*
|
||||
* @return {string} the message
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function _calculateWithheldMessage(withheld) {
|
||||
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
|
||||
return WITHHELD_MESSAGES[withheld.code];
|
||||
} else if (withheld.reason) {
|
||||
return withheld.reason;
|
||||
} else {
|
||||
return "decryption key withheld";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a received message with an inbound group session
|
||||
*
|
||||
@@ -934,16 +994,48 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
roomId, senderKey, sessionId, body, eventId, timestamp,
|
||||
) {
|
||||
let result;
|
||||
// when the localstorage crypto store is used as an indexeddb backend,
|
||||
// exceptions thrown from within the inner function are not passed through
|
||||
// to the top level, so we store exceptions in a variable and raise them at
|
||||
// the end
|
||||
let error;
|
||||
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData) => {
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
|
||||
if (session === null) {
|
||||
if (withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
_calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + '|' + sessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
const res = session.decrypt(body);
|
||||
let res;
|
||||
try {
|
||||
res = session.decrypt(body);
|
||||
} catch (e) {
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
_calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + '|' + sessionId,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
let plaintext = res.plaintext;
|
||||
if (plaintext === undefined) {
|
||||
@@ -965,7 +1057,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
msgInfo.id !== eventId ||
|
||||
msgInfo.timestamp !== timestamp
|
||||
) {
|
||||
throw new Error(
|
||||
error = new Error(
|
||||
"Duplicate message index, possible replay attack: " +
|
||||
messageIndexKey,
|
||||
);
|
||||
@@ -994,6 +1086,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1009,7 +1104,10 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData) => {
|
||||
if (sessionData === null) {
|
||||
@@ -1060,7 +1158,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData) => {
|
||||
if (session === null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +29,8 @@ const utils = require("../../utils");
|
||||
const olmlib = require("../olmlib");
|
||||
const base = require("./base");
|
||||
|
||||
import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
@@ -47,6 +50,7 @@ function OutboundSessionInfo(sessionId) {
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
this.blockedDevicesNotified = {};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +88,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function(
|
||||
this.sharedWithDevices[userId][deviceId] = chainIndex;
|
||||
};
|
||||
|
||||
OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function(
|
||||
userId, deviceId,
|
||||
) {
|
||||
if (!this.blockedDevicesNotified[userId]) {
|
||||
this.blockedDevicesNotified[userId] = {};
|
||||
}
|
||||
this.blockedDevicesNotified[userId][deviceId] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if this session has been shared with devices which it shouldn't
|
||||
* have been.
|
||||
@@ -166,11 +179,14 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
* @private
|
||||
*
|
||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
||||
* @param {Object} blocked The devices that are blocked, indexed by user ID
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
devicesInRoom, blocked,
|
||||
) {
|
||||
const self = this;
|
||||
|
||||
let session;
|
||||
@@ -237,9 +253,36 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
}
|
||||
}
|
||||
|
||||
return self._shareKeyWithDevices(
|
||||
await self._shareKeyWithDevices(
|
||||
session, shareMap,
|
||||
);
|
||||
|
||||
// are there any new blocked devices that we need to notify?
|
||||
const blockedMap = {};
|
||||
for (const userId in blocked) {
|
||||
if (!blocked.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const userBlockedDevices = blocked[userId];
|
||||
|
||||
for (const deviceId in userBlockedDevices) {
|
||||
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!session.blockedDevicesNotified[userId] ||
|
||||
session.blockedDevicesNotified[userId][deviceId] === undefined
|
||||
) {
|
||||
blockedMap[userId] = blockedMap[userId] || [];
|
||||
blockedMap[userId].push(userBlockedDevices[deviceId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notify blocked devices that they're blocked
|
||||
await self._notifyBlockedDevices(session, blockedMap);
|
||||
}
|
||||
|
||||
// helper which returns the session prepared by prepareSession
|
||||
@@ -363,6 +406,42 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
|
||||
return mapSlices;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {object} devicesByUser map from userid to list of devices
|
||||
*
|
||||
* @return {array<array<object>>} the blocked devices, split into chunks
|
||||
*/
|
||||
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
|
||||
const maxToDeviceMessagesPerRequest = 20;
|
||||
|
||||
// use an array where the slices of a content map gets stored
|
||||
let currentSlice = [];
|
||||
const mapSlices = [currentSlice];
|
||||
|
||||
for (const userId of Object.keys(devicesByUser)) {
|
||||
const userBlockedDevicesToShareWith = devicesByUser[userId];
|
||||
|
||||
for (const blockedInfo of userBlockedDevicesToShareWith) {
|
||||
if (currentSlice.length > maxToDeviceMessagesPerRequest) {
|
||||
// the current slice is filled up. Start inserting into the next slice
|
||||
currentSlice = [];
|
||||
mapSlices.push(currentSlice);
|
||||
}
|
||||
|
||||
currentSlice.push({
|
||||
userId: userId,
|
||||
blockedInfo: blockedInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (currentSlice.length === 0) {
|
||||
mapSlices.pop();
|
||||
}
|
||||
return mapSlices;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
@@ -427,6 +506,49 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*
|
||||
* @param {array<object>} userDeviceMap list of blocked devices to notify
|
||||
*
|
||||
* @param {object} payload fields to include in the notification payload
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the notifications
|
||||
* for the given userDeviceMap is generated and has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
|
||||
session, userDeviceMap, payload,
|
||||
) {
|
||||
const contentMap = {};
|
||||
|
||||
for (const val of userDeviceMap) {
|
||||
const userId = val.userId;
|
||||
const blockedInfo = val.blockedInfo;
|
||||
const deviceInfo = blockedInfo.deviceInfo;
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
|
||||
const message = Object.assign({}, payload);
|
||||
message.code = blockedInfo.code;
|
||||
message.reason = blockedInfo.reason;
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
}
|
||||
contentMap[userId][deviceId] = message;
|
||||
}
|
||||
|
||||
await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
|
||||
|
||||
// store that we successfully uploaded the keys of the current slice
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
session.markNotifiedBlockedDevice(userId, deviceId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-shares a megolm session key with devices if the key has already been
|
||||
* sent to them.
|
||||
@@ -561,6 +683,42 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify blocked devices that they have been blocked.
|
||||
*
|
||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*
|
||||
* @param {object<string, object>} devicesByUser
|
||||
* map from userid to device ID to blocked data
|
||||
*/
|
||||
MegolmEncryption.prototype._notifyBlockedDevices = async function(
|
||||
session, devicesByUser,
|
||||
) {
|
||||
const payload = {
|
||||
room_id: this._roomId,
|
||||
session_id: session.sessionId,
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
};
|
||||
|
||||
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
|
||||
|
||||
for (let i = 0; i < userDeviceMaps.length; i++) {
|
||||
try {
|
||||
await this._sendBlockedNotificationsToDevices(
|
||||
session, userDeviceMaps[i], payload,
|
||||
);
|
||||
logger.log(`Completed blacklist notification for ${session.sessionId} `
|
||||
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
} catch (e) {
|
||||
logger.log(`blacklist notification for ${session.sessionId} in `
|
||||
+ `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
@@ -570,42 +728,41 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
|
||||
const self = this;
|
||||
logger.log(`Starting to encrypt event for ${this._roomId}`);
|
||||
|
||||
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
// check if any of these devices are not yet known to the user.
|
||||
// if so, warn the user so they can verify or ignore.
|
||||
self._checkForUnknownDevices(devicesInRoom);
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
|
||||
return self._ensureOutboundSession(devicesInRoom);
|
||||
}).then(function(session) {
|
||||
const payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
// check if any of these devices are not yet known to the user.
|
||||
// if so, warn the user so they can verify or ignore.
|
||||
self._checkForUnknownDevices(devicesInRoom);
|
||||
|
||||
const ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
session.sessionId, JSON.stringify(payloadJson),
|
||||
);
|
||||
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
|
||||
const payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext,
|
||||
session_id: session.sessionId,
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
// XXX: Do we still need this now that m.new_device messages
|
||||
// no longer exist since #483?
|
||||
device_id: self._deviceId,
|
||||
};
|
||||
const ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
session.sessionId, JSON.stringify(payloadJson),
|
||||
);
|
||||
|
||||
session.useCount++;
|
||||
return encryptedContent;
|
||||
});
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext,
|
||||
session_id: session.sessionId,
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
// XXX: Do we still need this now that m.new_device messages
|
||||
// no longer exist since #483?
|
||||
device_id: self._deviceId,
|
||||
};
|
||||
|
||||
session.useCount++;
|
||||
return encryptedContent;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -654,8 +811,11 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to a map
|
||||
* from userId to deviceId to deviceInfo
|
||||
* @return {module:client.Promise} Promise which resolves to an array whose
|
||||
* first element is a map from userId to deviceId to deviceInfo indicating
|
||||
* the devices that messages should be encrypted to, and whose second
|
||||
* element is a map from userId to deviceId to data indicating the devices
|
||||
* that are in the room but that have been blocked
|
||||
*/
|
||||
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
@@ -676,6 +836,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
// using all the device_lists changes and left fields.
|
||||
// See https://github.com/vector-im/riot-web/issues/2305 for details.
|
||||
const devices = await this._crypto.downloadKeys(roomMembers, false);
|
||||
const blocked = {};
|
||||
// remove any blocked devices
|
||||
for (const userId in devices) {
|
||||
if (!devices.hasOwnProperty(userId)) {
|
||||
@@ -690,13 +851,27 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
|
||||
if (userDevices[deviceId].isBlocked() ||
|
||||
(userDevices[deviceId].isUnverified() && isBlacklisting)
|
||||
) {
|
||||
) {
|
||||
if (!blocked[userId]) {
|
||||
blocked[userId] = {};
|
||||
}
|
||||
const blockedInfo = userDevices[deviceId].isBlocked()
|
||||
? {
|
||||
code: "m.blacklisted",
|
||||
reason: WITHHELD_MESSAGES["m.blacklisted"],
|
||||
}
|
||||
: {
|
||||
code: "m.unverified",
|
||||
reason: WITHHELD_MESSAGES["m.unverified"],
|
||||
};
|
||||
blockedInfo.deviceInfo = userDevices[deviceId];
|
||||
blocked[userId][deviceId] = blockedInfo;
|
||||
delete userDevices[deviceId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
return [devices, blocked];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -756,6 +931,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
event.getId(), event.getTs(),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.name === "DecryptionError") {
|
||||
// re-throw decryption errors as-is
|
||||
throw e;
|
||||
}
|
||||
|
||||
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
|
||||
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
@@ -963,6 +1143,20 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
const content = event.getContent();
|
||||
|
||||
await this._olmDevice.addInboundGroupSessionWithheld(
|
||||
content.room_id, content.sender_key, content.session_id, content.code,
|
||||
content.reason,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -2408,6 +2408,8 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
||||
this._secretStorage._onRequestReceived(event);
|
||||
} else if (event.getType() === "m.secret.send") {
|
||||
this._secretStorage._onSecretReceived(event);
|
||||
} else if (event.getType() === "org.matrix.room_key.withheld") {
|
||||
this._onRoomKeyWithheldEvent(event);
|
||||
} else if (event.getContent().transaction_id) {
|
||||
this._onKeyVerificationMessage(event);
|
||||
} else if (event.getContent().msgtype === "m.bad.encrypted") {
|
||||
@@ -2447,6 +2449,27 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
alg.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key withheld event
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event key withheld event
|
||||
*/
|
||||
Crypto.prototype._onRoomKeyWithheldEvent = function(event) {
|
||||
const content = event.getContent();
|
||||
|
||||
if (!content.room_id || !content.session_id || !content.algorithm
|
||||
|| !content.sender_key) {
|
||||
logger.error("key withheld event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||
if (alg.onRoomKeyWithheldEvent) {
|
||||
alg.onRoomKeyWithheldEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a general key verification event.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +19,7 @@ limitations under the License.
|
||||
import logger from '../../logger';
|
||||
import utils from '../../utils';
|
||||
|
||||
export const VERSION = 7;
|
||||
export const VERSION = 8;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -79,7 +80,7 @@ export class Backend {
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
txn.oncomplete = () => { resolve(request); };
|
||||
txn.oncomplete = () => {resolve(request);};
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
@@ -428,14 +429,36 @@ export class Backend {
|
||||
// Inbound group sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
let session = false;
|
||||
let withheld = false;
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
if (getReq.result) {
|
||||
func(getReq.result.session);
|
||||
session = getReq.result.session;
|
||||
} else {
|
||||
func(null);
|
||||
session = null;
|
||||
}
|
||||
if (withheld !== false) {
|
||||
func(session, withheld);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
|
||||
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
|
||||
withheldGetReq.onsuccess = function() {
|
||||
try {
|
||||
if (withheldGetReq.result) {
|
||||
withheld = withheldGetReq.result.session;
|
||||
} else {
|
||||
withheld = null;
|
||||
}
|
||||
if (session !== false) {
|
||||
func(session, withheld);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
@@ -499,6 +522,15 @@ export class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
objectStore.put({
|
||||
senderCurve25519Key, sessionId, session: sessionData,
|
||||
});
|
||||
}
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
const objectStore = txn.objectStore("device_data");
|
||||
const getReq = objectStore.get("-");
|
||||
@@ -662,6 +694,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -104,7 +105,10 @@ export default class IndexedDBCryptoStore {
|
||||
// we can fall back to a different backend.
|
||||
return backend.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
],
|
||||
(txn) => {
|
||||
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
|
||||
}).then(() => {
|
||||
@@ -471,6 +475,16 @@ export default class IndexedDBCryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
|
||||
/**
|
||||
@@ -607,6 +621,8 @@ export default class IndexedDBCryptoStore {
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
|
||||
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||
= 'inbound_group_sessions_withheld';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +33,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
|
||||
|
||||
@@ -43,6 +45,10 @@ function keyEndToEndInboundGroupSession(senderKey, sessionId) {
|
||||
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
|
||||
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
function keyEndToEndRoomsPrefix(roomId) {
|
||||
return KEY_ROOMS_PREFIX + roomId;
|
||||
}
|
||||
@@ -125,10 +131,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
// Inbound Group Sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
func(getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
|
||||
));
|
||||
func(
|
||||
getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
|
||||
),
|
||||
getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
@@ -170,6 +182,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
setJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
|
||||
sessionData,
|
||||
);
|
||||
}
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
func(getJsonItem(
|
||||
this.store, KEY_DEVICE_DATA,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -37,6 +38,7 @@ export default class MemoryCryptoStore {
|
||||
this._sessions = {};
|
||||
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
|
||||
this._inboundGroupSessions = {};
|
||||
this._inboundGroupSessionsWithheld = {};
|
||||
// Opaque device data object
|
||||
this._deviceData = null;
|
||||
// roomId -> Opaque roomInfo object
|
||||
@@ -276,7 +278,11 @@ export default class MemoryCryptoStore {
|
||||
// Inbound Group Sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
|
||||
const k = senderCurve25519Key+'/'+sessionId;
|
||||
func(
|
||||
this._inboundGroupSessions[k] || null,
|
||||
this._inboundGroupSessionsWithheld[k] || null,
|
||||
);
|
||||
}
|
||||
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
@@ -306,6 +312,13 @@ export default class MemoryCryptoStore {
|
||||
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
const k = senderCurve25519Key+'/'+sessionId;
|
||||
this._inboundGroupSessionsWithheld[k] = sessionData;
|
||||
}
|
||||
|
||||
// Device Data
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
|
||||
Reference in New Issue
Block a user