1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-29 16:43:09 +03:00

Merge branch 'develop' into travis/sourcemaps

This commit is contained in:
Travis Ralston
2020-01-15 09:03:21 -07:00
10 changed files with 741 additions and 38 deletions

View File

@@ -451,6 +451,99 @@ describe("MegolmDecryption", function() {
bobClient2.stopClient(); bobClient2.stopClient();
}); });
it("notifies devices when unable to create olm session", 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 aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice = bobClient._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
aliceRoom.getEncryptionTargetMembers = async () => {
return [
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
];
};
const BOB_DEVICES = {
bobdevice: {
user_id: "@bob:example.com",
device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
},
known: true,
verified: 1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
// Bob has no one-time keys
return {
one_time_keys: {},
};
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
expect(run).toBe(true);
});
it("throws an error describing why it doesn't have a key", async function() { it("throws an error describing why it doesn't have a key", async function() {
const aliceClient = (new TestClient( const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice", "@alice:example.com", "alicedevice",
@@ -493,4 +586,103 @@ describe("MegolmDecryption", function() {
}, },
}))).rejects.toThrow("The sender has blocked you."); }))).rejects.toThrow("The sender has blocked you.");
}); });
it("throws an error describing the lack of an olm session", 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";
const now = Date.now();
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.no_olm",
reason: "Unable to establish a secure channel.",
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
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",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
});
it("throws an error to indicate a wedged olm session", 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";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
msgtype: "m.bad.encrypted",
algorithm: "m.megolm.v1.aes-sha2",
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
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",
},
origin_server_ts: now,
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
});
}); });

View File

@@ -940,6 +940,35 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
return this._crypto.getGlobalBlacklistUnverifiedDevices(); return this._crypto.getGlobalBlacklistUnverifiedDevices();
}; };
/**
* Set whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send them message. This has 'Global' for
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* @param {boolean} value whether error on unknown devices
*/
MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function(value) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.setGlobalErrorOnUnknownDevices(value);
};
/**
* @return {boolean} whether to error on unknown devices
*
* This API is currently UNSTABLE and may change or be removed without notice.
*/
MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getGlobalErrorOnUnknownDevices();
};
/** /**
* Add methods that call the corresponding method in this._crypto * Add methods that call the corresponding method in this._crypto
* *

View File

@@ -673,6 +673,18 @@ OlmDevice.prototype.matchesSession = async function(
return matches; return matches;
}; };
OlmDevice.prototype.recordSessionProblem = async function(deviceKey, type, fixed) {
await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
};
OlmDevice.prototype.sessionMayHaveProblems = async function(deviceKey, timestamp) {
return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
};
OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
};
// Outbound group session // Outbound group session
// ====================== // ======================

View File

@@ -158,6 +158,16 @@ export class DecryptionAlgorithm {
shareKeysWithDevice(keyRequest) { shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
} }
/**
* Retry decrypting all the events from a sender that haven't been
* decrypted yet.
*
* @param {string} senderKey the sender's key
*/
async retryDecryptionFromSender(senderKey) {
// ignore by default
}
} }
/** /**

View File

@@ -258,8 +258,10 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
} }
} }
const errorDevices = [];
await self._shareKeyWithDevices( await self._shareKeyWithDevices(
session, shareMap, session, shareMap, errorDevices,
); );
// are there any new blocked devices that we need to notify? // are there any new blocked devices that we need to notify?
@@ -286,6 +288,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
} }
} }
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked // notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap); await self._notifyBlockedDevices(session, blockedMap);
} }
@@ -335,6 +349,10 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
}; };
/** /**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
*
* @private * @private
* *
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
@@ -347,12 +365,16 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser * @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices * map from userid to list of devices
* *
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>} * @return {array<object<userid, deviceInfo>>}
*/ */
MegolmEncryption.prototype._splitUserDeviceMap = function( MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser, session, chainIndex, devicemap, devicesByUser, errorDevices,
) { ) {
const maxToDeviceMessagesPerRequest = 20; const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored // use an array where the slices of a content map gets stored
const mapSlices = []; const mapSlices = [];
@@ -382,6 +404,8 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
// to claim a one-time-key for dead devices on every message. // to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex); session.markSharedWithDevice(userId, deviceId, chainIndex);
errorDevices.push({userId, deviceInfo});
// ensureOlmSessionsForUsers has already done the logging, // ensureOlmSessionsForUsers has already done the logging,
// so just skip it. // so just skip it.
continue; continue;
@@ -391,11 +415,6 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
"share keys with device " + userId + ":" + deviceId, "share keys with device " + userId + ":" + deviceId,
); );
if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
if (!mapSlices[currentSliceId]) { if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = []; mapSlices[currentSliceId] = [];
} }
@@ -407,11 +426,25 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
entriesInCurrentSlice++; entriesInCurrentSlice++;
} }
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
} }
return mapSlices; return mapSlices;
}; };
/** /**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call.
*
* @private * @private
* *
* @param {object} devicesByUser map from userid to list of devices * @param {object} devicesByUser map from userid to list of devices
@@ -419,7 +452,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
* @return {array<array<object>>} the blocked devices, split into chunks * @return {array<array<object>>} the blocked devices, split into chunks
*/ */
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxToDeviceMessagesPerRequest = 20; const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored // use an array where the slices of a content map gets stored
let currentSlice = []; let currentSlice = [];
@@ -429,17 +462,22 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const userBlockedDevicesToShareWith = devicesByUser[userId]; const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) { 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({ currentSlice.push({
userId: userId, userId: userId,
blockedInfo: blockedInfo, blockedInfo: blockedInfo,
}); });
} }
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
}
} }
if (currentSlice.length === 0) { if (currentSlice.length === 0) {
mapSlices.pop(); mapSlices.pop();
@@ -537,6 +575,10 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
const message = Object.assign({}, payload); const message = Object.assign({}, payload);
message.code = blockedInfo.code; message.code = blockedInfo.code;
message.reason = blockedInfo.reason; message.reason = blockedInfo.reason;
if (message.code === "m.no_olm") {
delete message.room_id;
delete message.session_id;
}
if (!contentMap[userId]) { if (!contentMap[userId]) {
contentMap[userId] = {}; contentMap[userId] = {};
@@ -650,8 +692,14 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
* *
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser * @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices * map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*/ */
MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) { MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, devicesByUser, errorDevices,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = { const payload = {
type: "m.room_key", type: "m.room_key",
@@ -669,7 +717,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
); );
const userDeviceMaps = this._splitUserDeviceMap( const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser, session, key.chain_index, devicemap, devicesByUser, errorDevices,
); );
for (let i = 0; i < userDeviceMaps.length; i++) { for (let i = 0; i < userDeviceMaps.length; i++) {
@@ -741,7 +789,9 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
// check if any of these devices are not yet known to the user. // check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore. // if so, warn the user so they can verify or ignore.
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom); self._checkForUnknownDevices(devicesInRoom);
}
const session = await self._ensureOutboundSession(devicesInRoom, blocked); const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = { const payloadJson = {
@@ -900,6 +950,11 @@ function MegolmDecryption(params) {
} }
utils.inherits(MegolmDecryption, DecryptionAlgorithm); utils.inherits(MegolmDecryption, DecryptionAlgorithm);
const PROBLEM_DESCRIPTIONS = {
no_olm: "The sender was unable to establish a secure channel.",
unknown: "The secure channel with the sender was corrupted.",
};
/** /**
* @inheritdoc * @inheritdoc
* *
@@ -966,7 +1021,33 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// event is still in the pending list; if not, a retry will have been // event is still in the pending list; if not, a retry will have been
// scheduled, so we needn't send out the request here.) // scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event); this._requestKeysForEvent(event);
<<<<<<< HEAD
throw new DecryptionError( throw new DecryptionError(
=======
// See if there was a problem with the olm session at the time the
// event was sent. Use a fuzz factor of 2 minutes.
const problem = await this._olmDevice.sessionMayHaveProblems(
content.sender_key, event.getTs() - 120000,
);
if (problem) {
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|| PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
" Trying to create a new secure channel and re-requesting the keys.";
}
throw new base.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
problemDescription,
{
session: content.sender_key + '|' + content.session_id,
},
);
}
throw new base.DecryptionError(
>>>>>>> develop
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.", "The sender's device has not sent us the keys for this message.",
{ {
@@ -1021,11 +1102,16 @@ MegolmDecryption.prototype._requestKeysForEvent = function(event) {
*/ */
MegolmDecryption.prototype._addEventToPendingList = function(event) { MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent(); const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id; const senderKey = content.sender_key;
if (!this._pendingEvents[k]) { const sessionId = content.session_id;
this._pendingEvents[k] = new Set(); if (!this._pendingEvents[senderKey]) {
this._pendingEvents[senderKey] = new Map();
} }
this._pendingEvents[k].add(event); const senderPendingEvents = this._pendingEvents[senderKey];
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
senderPendingEvents.get(sessionId).add(event);
}; };
/** /**
@@ -1037,14 +1123,20 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
*/ */
MegolmDecryption.prototype._removeEventFromPendingList = function(event) { MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
const content = event.getWireContent(); const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id; const senderKey = content.sender_key;
if (!this._pendingEvents[k]) { const sessionId = content.session_id;
const senderPendingEvents = this._pendingEvents[senderKey];
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
if (!pendingEvents) {
return; return;
} }
this._pendingEvents[k].delete(event); pendingEvents.delete(event);
if (this._pendingEvents[k].size === 0) { if (pendingEvents.size === 0) {
delete this._pendingEvents[k]; senderPendingEvents.delete(senderKey);
}
if (senderPendingEvents.size === 0) {
delete this._pendingEvents[senderKey];
} }
}; };
@@ -1155,11 +1247,69 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
*/ */
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
const content = event.getContent(); const content = event.getContent();
const senderKey = content.sender_key;
if (content.code === "m.no_olm") {
const sender = event.getSender();
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
// Note: after we record that the olm session has had a problem, we
// trigger retrying decryption for all the messages from the sender's
// key, so that we can update the error message to indicate the olm
// session problem.
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
sender,
device,
{type: "m.dummy"},
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
await this._baseApis.sendToDevice("m.room.encrypted", {
[sender]: {
[device.deviceId]: encryptedContent,
},
});
} else {
await this._olmDevice.addInboundGroupSessionWithheld( await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, content.sender_key, content.session_id, content.code, content.room_id, senderKey, content.session_id, content.code,
content.reason, content.reason,
); );
}
}; };
/** /**
@@ -1305,13 +1455,20 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @return {Boolean} whether all messages were successfully decrypted * @return {Boolean} whether all messages were successfully decrypted
*/ */
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) { MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId; const senderPendingEvents = this._pendingEvents[senderKey];
const pending = this._pendingEvents[k]; if (!senderPendingEvents) {
return true;
}
const pending = senderPendingEvents.get(sessionId);
if (!pending) { if (!pending) {
return true; return true;
} }
delete this._pendingEvents[k]; pending.delete(sessionId);
if (pending.size === 0) {
this._pendingEvents[senderKey];
}
await Promise.all([...pending].map(async (ev) => { await Promise.all([...pending].map(async (ev) => {
try { try {
@@ -1321,7 +1478,32 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
} }
})); }));
return !this._pendingEvents[k]; // ev.attemptDecryption will re-add to this._pendingEvents if an event
// couldn't be decrypted
return !((this._pendingEvents[senderKey] || {})[sessionId]);
};
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
delete this._pendingEvents[senderKey];
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
}));
return !this._pendingEvents[senderKey];
}; };
registerAlgorithm( registerAlgorithm(

View File

@@ -173,6 +173,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
this._deviceKeys = {}; this._deviceKeys = {};
this._globalBlacklistUnverifiedDevices = false; this._globalBlacklistUnverifiedDevices = false;
this._globalErrorOnUnknownDevices = true;
this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
baseApis, this._deviceId, this._cryptoStore, baseApis, this._deviceId, this._cryptoStore,
@@ -1206,6 +1207,29 @@ Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function() {
return this._globalBlacklistUnverifiedDevices; return this._globalBlacklistUnverifiedDevices;
}; };
/**
* Set whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send them message. This has 'Global' for
* symmertry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* @param {boolean} value whether error on unknown devices
*/
Crypto.prototype.setGlobalErrorOnUnknownDevices = function(value) {
this._globalErrorOnUnknownDevices = value;
};
/**
* @return {boolean} whether to error on unknown devices
*
* This API is currently UNSTABLE and may change or be removed without notice.
*/
Crypto.prototype.getGlobalErrorOnUnknownDevices = function() {
return this._globalErrorOnUnknownDevices;
};
/** /**
* Upload the device keys to the homeserver. * Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded. * @return {object} A promise that will resolve when the keys are uploaded.
@@ -2474,16 +2498,31 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
Crypto.prototype._onRoomKeyWithheldEvent = function(event) { Crypto.prototype._onRoomKeyWithheldEvent = function(event) {
const content = event.getContent(); const content = event.getContent();
if (!content.room_id || !content.session_id || !content.algorithm if ((content.code !== "m.no_olm" && (!content.room_id || !content.session_id))
|| !content.sender_key) { || !content.algorithm || !content.sender_key) {
logger.error("key withheld event is missing fields"); logger.error("key withheld event is missing fields");
return; return;
} }
logger.info(
`Got room key withheld event from ${event.getSender()} (${content.sender_key}) `
+ `for ${content.algorithm}/${content.room_id}/${content.session_id} `
+ `with reason ${content.code} (${content.reason})`,
);
const alg = this._getRoomDecryptor(content.room_id, content.algorithm); const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
if (alg.onRoomKeyWithheldEvent) { if (alg.onRoomKeyWithheldEvent) {
alg.onRoomKeyWithheldEvent(event); alg.onRoomKeyWithheldEvent(event);
} }
if (!content.room_id) {
// retry decryption for all events sent by the sender_key. This will
// update the events to show a message indicating that the olm session was
// wedged.
const roomDecryptors = this._getRoomDecryptors(content.algorithm);
for (const decryptor of roomDecryptors) {
decryptor.retryDecryptionFromSender(content.sender_key);
}
}
}; };
/** /**
@@ -2599,6 +2638,16 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
const algorithm = content.algorithm; const algorithm = content.algorithm;
const deviceKey = content.sender_key; const deviceKey = content.sender_key;
// retry decryption for all events sent by the sender_key. This will
// update the events to show a message indicating that the olm session was
// wedged.
const retryDecryption = () => {
const roomDecryptors = this._getRoomDecryptors(olmlib.MEGOLM_ALGORITHM);
for (const decryptor of roomDecryptors) {
decryptor.retryDecryptionFromSender(deviceKey);
}
};
if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
return; return;
} }
@@ -2612,6 +2661,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
"New session already forced with device " + sender + ":" + deviceKey + "New session already forced with device " + sender + ":" + deviceKey +
" at " + lastNewSessionForced + ": not forcing another", " at " + lastNewSessionForced + ": not forcing another",
); );
await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true);
retryDecryption();
return; return;
} }
@@ -2625,6 +2676,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
"Couldn't find device for identity key " + deviceKey + "Couldn't find device for identity key " + deviceKey +
": not re-establishing session", ": not re-establishing session",
); );
await this._olmDevice.recordSessionProblem(deviceKey, "wedged", false);
retryDecryption();
return; return;
} }
const devicesByUser = {}; const devicesByUser = {};
@@ -2656,6 +2709,9 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
{type: "m.dummy"}, {type: "m.dummy"},
); );
await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true);
retryDecryption();
await this._baseApis.sendToDevice("m.room.encrypted", { await this._baseApis.sendToDevice("m.room.encrypted", {
[sender]: { [sender]: {
[device.deviceId]: encryptedContent, [device.deviceId]: encryptedContent,
@@ -2937,6 +2993,24 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) {
}; };
/**
* Get all the room decryptors for a given encryption algorithm.
*
* @param {string} algorithm The encryption algorithm
*
* @return {array} An array of room decryptors
*/
Crypto.prototype._getRoomDecryptors = function(algorithm) {
const decryptors = [];
for (const d of Object.values(this._roomDecryptors)) {
if (algorithm in d) {
decryptors.push(d[algorithm]);
}
}
return decryptors;
};
/** /**
* sign the given object with our ed25519 key * sign the given object with our ed25519 key
* *

View File

@@ -19,7 +19,7 @@ limitations under the License.
import {logger} from '../../logger'; import {logger} from '../../logger';
import * as utils from "../../utils"; import * as utils from "../../utils";
export const VERSION = 8; export const VERSION = 9;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -426,6 +426,74 @@ export class Backend {
}); });
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
objectStore.put({
deviceKey,
type,
fixed,
time: Date.now(),
});
return promiseifyTxn(txn);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
let result;
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = (event) => {
const problems = req.result;
if (!problems.length) {
result = null;
return;
}
problems.sort((a, b) => {
return a.time - b.time;
});
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
result = Object.assign({}, problem, {fixed: lastProblem.fixed});
return;
}
}
if (lastProblem.fixed) {
result = null;
} else {
result = lastProblem;
}
};
await promiseifyTxn(txn);
return result;
}
// FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) {
const txn = this._db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices");
const ret = [];
await Promise.all(devices.map((device) => {
return new Promise((resolve) => {
const {userId, deviceInfo} = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
if (!getReq.result) {
objectStore.put({userId, deviceId: deviceInfo.deviceId});
ret.push(device);
}
resolve();
};
});
}));
return ret;
}
// Inbound group sessions // Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
@@ -699,6 +767,16 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} }
if (oldVersion < 9) {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
problemsStore.createIndex("deviceKey", "deviceKey");
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -409,6 +409,24 @@ export class IndexedDBCryptoStore {
}); });
} }
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backendPromise.then(async (backend) => {
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
});
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backendPromise.then(async (backend) => {
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
});
}
filterOutNotifiedErrorDevices(devices) {
return this._backendPromise.then(async (backend) => {
return await backend.filterOutNotifiedErrorDevices(devices);
});
}
// Inbound group sessions // Inbound group sessions
/** /**

View File

@@ -31,6 +31,7 @@ import {MemoryCryptoStore} from './memory-crypto-store';
const E2E_PREFIX = "crypto."; const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; 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_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
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_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
@@ -41,6 +42,10 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey; return E2E_PREFIX + "sessions/" + deviceKey;
} }
function keyEndToEndSessionProblems(deviceKey) {
return E2E_PREFIX + "session.problems/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) { function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
} }
@@ -128,6 +133,58 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices =
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
return ret;
}
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {

View File

@@ -36,6 +36,10 @@ export class MemoryCryptoStore {
// Map of {devicekey -> {sessionId -> session pickle}} // Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {}; this._sessions = {};
// Map of {devicekey -> array of problems}
this._sessionProblems = {};
// Map of {userId -> deviceId -> true}
this._notifiedErrorDevices = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object} // Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {}; this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {}; this._inboundGroupSessionsWithheld = {};
@@ -275,6 +279,53 @@ export class MemoryCryptoStore {
deviceSessions[sessionId] = sessionInfo; deviceSessions[sessionId] = sessionInfo;
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const problems = this._sessionProblems[deviceKey]
= this._sessionProblems[deviceKey] || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const problems = this._sessionProblems[deviceKey] || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices = this._notifiedErrorDevices;
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
return ret;
}
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {