1
0
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:
Hubert Chathi
2020-01-06 17:47:22 -05:00
committed by GitHub
parent 64fb79e0be
commit 01f6b3dfc6
9 changed files with 621 additions and 56 deletions

View File

@@ -617,6 +617,9 @@ describe("megolm", function() {
).respond(200, { ).respond(200, {
event_id: '$event_id', event_id: '$event_id',
}); });
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([ return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'), aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -714,6 +717,9 @@ describe("megolm", function() {
event_id: '$event_id', event_id: '$event_id',
}; };
}); });
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([ return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'), aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),

View File

@@ -8,6 +8,9 @@ import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice'; import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto'; import Crypto from '../../../../lib/crypto';
import logger from '../../../../lib/logger'; 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 MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; 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); 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.");
});
}); });

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017, 2019 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 logger from '../logger';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; 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 // 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. // reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
@@ -818,9 +821,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, txn, func, roomId, senderKey, sessionId, txn, func,
) { ) {
this._cryptoStore.getEndToEndInboundGroupSession( this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => { senderKey, sessionId, txn, (sessionData, withheld) => {
if (sessionData === null) { if (sessionData === null) {
func(null); func(null, null, withheld);
return; return;
} }
@@ -834,7 +837,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
} }
this._unpickleInboundGroupSession(sessionData, (session) => { this._unpickleInboundGroupSession(sessionData, (session) => {
func(session, sessionData); func(session, sessionData, withheld);
}); });
}, },
); );
@@ -859,7 +862,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
exportFormat, exportFormat,
) { ) {
await this._cryptoStore.doTxn( 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 */ /* if we already have this session, consider updating it */
this._getInboundGroupSession( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, 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 * Decrypt a received message with an inbound group session
* *
@@ -934,16 +994,48 @@ OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp, roomId, senderKey, sessionId, body, eventId, timestamp,
) { ) {
let result; 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( 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( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => { roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) { if (session === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
}
result = null; result = null;
return; 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; let plaintext = res.plaintext;
if (plaintext === undefined) { if (plaintext === undefined) {
@@ -965,7 +1057,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
msgInfo.id !== eventId || msgInfo.id !== eventId ||
msgInfo.timestamp !== timestamp msgInfo.timestamp !== timestamp
) { ) {
throw new Error( error = new Error(
"Duplicate message index, possible replay attack: " + "Duplicate message index, possible replay attack: " +
messageIndexKey, messageIndexKey,
); );
@@ -994,6 +1086,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
}, },
); );
if (error) {
throw error;
}
return result; return result;
}; };
@@ -1009,7 +1104,10 @@ OlmDevice.prototype.decryptGroupMessage = async function(
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) { OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
let result; let result;
await this._cryptoStore.doTxn( 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( this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => { senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) { if (sessionData === null) {
@@ -1060,7 +1158,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
) { ) {
let result; let result;
await this._cryptoStore.doTxn( 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( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => { roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) { if (session === null) {

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 olmlib = require("../olmlib");
const base = require("./base"); const base = require("./base");
import {WITHHELD_MESSAGES} from '../OlmDevice';
/** /**
* @private * @private
* @constructor * @constructor
@@ -47,6 +50,7 @@ function OutboundSessionInfo(sessionId) {
this.useCount = 0; this.useCount = 0;
this.creationTime = new Date().getTime(); this.creationTime = new Date().getTime();
this.sharedWithDevices = {}; this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
} }
@@ -84,6 +88,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function(
this.sharedWithDevices[userId][deviceId] = chainIndex; 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 * Determine if this session has been shared with devices which it shouldn't
* have been. * have been.
@@ -166,11 +179,14 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
* @private * @private
* *
* @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
* *
* @return {module:client.Promise} Promise which resolves to the * @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete. * OutboundSessionInfo when setup is complete.
*/ */
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
) {
const self = this; const self = this;
let session; let session;
@@ -237,9 +253,36 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
} }
} }
return self._shareKeyWithDevices( await self._shareKeyWithDevices(
session, shareMap, 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 // helper which returns the session prepared by prepareSession
@@ -363,6 +406,42 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
return mapSlices; 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 * @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 * Re-shares a megolm session key with devices if the key has already been
* sent to them. * 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 * @inheritdoc
* *
@@ -570,42 +728,41 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
* *
* @return {module:client.Promise} Promise which resolves to the new event body * @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; const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`); logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) { const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// 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);
return self._ensureOutboundSession(devicesInRoom); // check if any of these devices are not yet known to the user.
}).then(function(session) { // if so, warn the user so they can verify or ignore.
const payloadJson = { self._checkForUnknownDevices(devicesInRoom);
room_id: self._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage( const session = await self._ensureOutboundSession(devicesInRoom, blocked);
session.sessionId, JSON.stringify(payloadJson), const payloadJson = {
); room_id: self._roomId,
type: eventType,
content: content,
};
const encryptedContent = { const ciphertext = self._olmDevice.encryptGroupMessage(
algorithm: olmlib.MEGOLM_ALGORITHM, session.sessionId, JSON.stringify(payloadJson),
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++; const encryptedContent = {
return 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 * @param {module:models/room} room
* *
* @return {module:client.Promise} Promise which resolves to a map * @return {module:client.Promise} Promise which resolves to an array whose
* from userId to deviceId to deviceInfo * 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) { MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers(); const members = await room.getEncryptionTargetMembers();
@@ -676,6 +836,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
// using all the device_lists changes and left fields. // using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details. // See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false); const devices = await this._crypto.downloadKeys(roomMembers, false);
const blocked = {};
// remove any blocked devices // remove any blocked devices
for (const userId in devices) { for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) { if (!devices.hasOwnProperty(userId)) {
@@ -690,13 +851,27 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
if (userDevices[deviceId].isBlocked() || if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting) (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]; delete userDevices[deviceId];
} }
} }
} }
return devices; return [devices, blocked];
}; };
/** /**
@@ -756,6 +931,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(), event.getId(), event.getTs(),
); );
} catch (e) { } catch (e) {
if (e.name === "DecryptionError") {
// re-throw decryption errors as-is
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { 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 * @inheritdoc
*/ */

View File

@@ -2,7 +2,7 @@
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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); this._secretStorage._onRequestReceived(event);
} else if (event.getType() === "m.secret.send") { } else if (event.getType() === "m.secret.send") {
this._secretStorage._onSecretReceived(event); this._secretStorage._onSecretReceived(event);
} else if (event.getType() === "org.matrix.room_key.withheld") {
this._onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) { } else if (event.getContent().transaction_id) {
this._onKeyVerificationMessage(event); this._onKeyVerificationMessage(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") { } else if (event.getContent().msgtype === "m.bad.encrypted") {
@@ -2447,6 +2449,27 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
alg.onRoomKeyEvent(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. * Handle a general key verification event.
* *

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 logger from '../../logger';
import utils from '../../utils'; import utils from '../../utils';
export const VERSION = 7; export const VERSION = 8;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -79,7 +80,7 @@ export class Backend {
`enqueueing key request for ${requestBody.room_id} / ` + `enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id, requestBody.session_id,
); );
txn.oncomplete = () => { resolve(request); }; txn.oncomplete = () => {resolve(request);};
const store = txn.objectStore("outgoingRoomKeyRequests"); const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request); store.add(request);
}); });
@@ -428,14 +429,36 @@ export class Backend {
// Inbound group sessions // Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]); const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
try { try {
if (getReq.result) { if (getReq.result) {
func(getReq.result.session); session = getReq.result.session;
} else { } 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) { } catch (e) {
abortWithException(txn, 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) { getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data"); const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-"); const getReq = objectStore.get("-");
@@ -662,6 +694,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} }
if (oldVersion < 8) {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. // we can fall back to a different backend.
return backend.doTxn( return backend.doTxn(
'readonly', 'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
],
(txn) => { (txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {}); backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => { }).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 // End-to-end device tracking
/** /**
@@ -607,6 +621,8 @@ export default class IndexedDBCryptoStore {
IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; 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
= 'inbound_group_sessions_withheld';
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';

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2017, 2018 New Vector Ltd 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; 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_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
@@ -43,6 +45,10 @@ function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
} }
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) { function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId; return KEY_ROOMS_PREFIX + roomId;
} }
@@ -125,10 +131,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem( func(
this.store, getJsonItem(
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), this.store,
)); keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
),
getJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
),
);
} }
getAllEndToEndInboundGroupSessions(txn, func) { 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) { getEndToEndDeviceData(txn, func) {
func(getJsonItem( func(getJsonItem(
this.store, KEY_DEVICE_DATA, this.store, KEY_DEVICE_DATA,

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -37,6 +38,7 @@ export default class MemoryCryptoStore {
this._sessions = {}; this._sessions = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object} // Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {}; this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {};
// Opaque device data object // Opaque device data object
this._deviceData = null; this._deviceData = null;
// roomId -> Opaque roomInfo object // roomId -> Opaque roomInfo object
@@ -276,7 +278,11 @@ export default class MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { 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) { getAllEndToEndInboundGroupSessions(txn, func) {
@@ -306,6 +312,13 @@ export default class MemoryCryptoStore {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
} }
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData;
}
// Device Data // Device Data
getEndToEndDeviceData(txn, func) { getEndToEndDeviceData(txn, func) {