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
record, report, and notify about olm errors
This commit is contained in:
@@ -11,6 +11,7 @@ import logger from '../../../../lib/logger';
|
|||||||
import TestClient from '../../../TestClient';
|
import TestClient from '../../../TestClient';
|
||||||
import olmlib from '../../../../lib/crypto/olmlib';
|
import olmlib from '../../../../lib/crypto/olmlib';
|
||||||
import Room from '../../../../lib/models/room';
|
import Room from '../../../../lib/models/room';
|
||||||
|
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||||
|
|
||||||
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'];
|
||||||
@@ -453,6 +454,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",
|
||||||
@@ -495,4 +589,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.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -674,6 +674,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
|
||||||
// ======================
|
// ======================
|
||||||
|
|||||||
@@ -253,8 +253,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?
|
||||||
@@ -281,6 +283,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);
|
||||||
}
|
}
|
||||||
@@ -345,7 +359,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
|||||||
* @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 maxToDeviceMessagesPerRequest = 20;
|
||||||
|
|
||||||
@@ -377,6 +391,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;
|
||||||
@@ -532,6 +548,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] = {};
|
||||||
@@ -646,7 +666,9 @@ 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
|
||||||
*/
|
*/
|
||||||
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",
|
||||||
@@ -664,7 +686,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++) {
|
||||||
@@ -895,6 +917,11 @@ function MegolmDecryption(params) {
|
|||||||
}
|
}
|
||||||
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
utils.inherits(MegolmDecryption, base.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
|
||||||
*
|
*
|
||||||
@@ -961,6 +988,26 @@ 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);
|
||||||
|
|
||||||
|
const problem = await this._olmDevice.sessionMayHaveProblems(
|
||||||
|
content.sender_key, event.getTs(),
|
||||||
|
);
|
||||||
|
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(
|
throw new base.DecryptionError(
|
||||||
"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.",
|
||||||
@@ -1150,11 +1197,60 @@ 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;
|
||||||
|
|
||||||
await this._olmDevice.addInboundGroupSessionWithheld(
|
if (content.code === "m.no_olm") {
|
||||||
content.room_id, content.sender_key, content.session_id, content.code,
|
const sender = event.getSender();
|
||||||
content.reason,
|
// if the sender says that they haven't been able to establish an olm
|
||||||
);
|
// session, let's proactively establish one
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
await this._baseApis.sendToDevice("m.room.encrypted", {
|
||||||
|
[sender]: {
|
||||||
|
[device.deviceId]: encryptedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this._olmDevice.addInboundGroupSessionWithheld(
|
||||||
|
content.room_id, senderKey, content.session_id, content.code,
|
||||||
|
content.reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2458,8 +2458,8 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -2609,6 +2609,9 @@ 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,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const devicesByUser = {};
|
const devicesByUser = {};
|
||||||
@@ -2640,6 +2643,8 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
|
|||||||
{type: "m.dummy"},
|
{type: "m.dummy"},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true);
|
||||||
|
|
||||||
await this._baseApis.sendToDevice("m.room.encrypted", {
|
await this._baseApis.sendToDevice("m.room.encrypted", {
|
||||||
[sender]: {
|
[sender]: {
|
||||||
[device.deviceId]: encryptedContent,
|
[device.deviceId]: encryptedContent,
|
||||||
|
|||||||
@@ -19,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 = 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,70 @@ 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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +763,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");
|
||||||
|
|
||||||
|
db.createObjectStore("notified_error_devices", {
|
||||||
|
keyPath: ["userId", "deviceId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
// Expand as needed.
|
// Expand as needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,6 +409,24 @@ export default 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) => {
|
||||||
|
await backend.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterOutNotifiedErrorDevices(devices) {
|
||||||
|
return this._backendPromise.then(async (backend) => {
|
||||||
|
return await backend.filterOutNotifiedErrorDevices(devices);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Inbound group sessions
|
// Inbound group sessions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 default 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) {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export default 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 default 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user