You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
Restart broken Olm sessions
* Start a new Olm sessions with a device when we get an undecryptable message on it. * Send a dummy message on that sessions such that the other end knows about it. * Re-send any outstanding keyshare requests for that device. Also includes a unit test for megolm that isn't very related but came out as a result anyway. Includes https://github.com/matrix-org/matrix-js-sdk/pull/776 Fixes https://github.com/vector-im/riot-web/issues/3822
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
|
|
||||||
"use strict";
|
|
||||||
import 'source-map-support/register';
|
import 'source-map-support/register';
|
||||||
import Crypto from '../../lib/crypto';
|
import Crypto from '../../lib/crypto';
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
|
|
||||||
|
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||||
|
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||||
|
import MockStorageApi from '../MockStorageApi';
|
||||||
|
|
||||||
|
const EventEmitter = require("events").EventEmitter;
|
||||||
|
|
||||||
const sdk = require("../..");
|
const sdk = require("../..");
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
@@ -20,4 +24,96 @@ describe("Crypto", function() {
|
|||||||
it("Crypto exposes the correct olm library version", function() {
|
it("Crypto exposes the correct olm library version", function() {
|
||||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Session management', function() {
|
||||||
|
const otkResponse = {
|
||||||
|
one_time_keys: {
|
||||||
|
'@alice:home.server': {
|
||||||
|
aliceDevice: {
|
||||||
|
'signed_curve25519:FLIBBLE': {
|
||||||
|
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||||
|
signatures: {
|
||||||
|
'@alice:home.server': {
|
||||||
|
'ed25519:aliceDevice': 'totally a valid signature',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let crypto;
|
||||||
|
let mockBaseApis;
|
||||||
|
let mockRoomList;
|
||||||
|
|
||||||
|
let fakeEmitter;
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
const mockStorage = new MockStorageApi();
|
||||||
|
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||||
|
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||||
|
|
||||||
|
cryptoStore.storeEndToEndDeviceData({
|
||||||
|
devices: {
|
||||||
|
'@bob:home.server': {
|
||||||
|
'BOBDEVICE': {
|
||||||
|
keys: {
|
||||||
|
'curve25519:BOBDEVICE': 'this is a key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trackingStatus: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockBaseApis = {
|
||||||
|
sendToDevice: expect.createSpy(),
|
||||||
|
};
|
||||||
|
mockRoomList = {};
|
||||||
|
|
||||||
|
fakeEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
crypto = new Crypto(
|
||||||
|
mockBaseApis,
|
||||||
|
sessionStore,
|
||||||
|
"@alice:home.server",
|
||||||
|
"FLIBBLE",
|
||||||
|
sessionStore,
|
||||||
|
cryptoStore,
|
||||||
|
mockRoomList,
|
||||||
|
);
|
||||||
|
crypto.registerEventHandlers(fakeEmitter);
|
||||||
|
await crypto.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function() {
|
||||||
|
await crypto.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restarts wedged Olm sessions", async function() {
|
||||||
|
const prom = new Promise((resolve) => {
|
||||||
|
mockBaseApis.claimOneTimeKeys = function() {
|
||||||
|
resolve();
|
||||||
|
return otkResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
fakeEmitter.emit('toDeviceEvent', {
|
||||||
|
getType: expect.createSpy().andReturn('m.room.message'),
|
||||||
|
getContent: expect.createSpy().andReturn({
|
||||||
|
msgtype: 'm.bad.encrypted',
|
||||||
|
}),
|
||||||
|
getWireContent: expect.createSpy().andReturn({
|
||||||
|
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
sender_key: 'this is a key',
|
||||||
|
}),
|
||||||
|
getSender: expect.createSpy().andReturn('@bob:home.server'),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("waiting");
|
||||||
|
await prom;
|
||||||
|
console.log("done");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -18,6 +18,7 @@ import Crypto from '../../../../lib/crypto';
|
|||||||
|
|
||||||
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'];
|
||||||
|
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||||
|
|
||||||
const ROOM_ID = '!ROOM:ID';
|
const ROOM_ID = '!ROOM:ID';
|
||||||
|
|
||||||
@@ -34,9 +35,11 @@ describe("MegolmDecryption", function() {
|
|||||||
let mockCrypto;
|
let mockCrypto;
|
||||||
let mockBaseApis;
|
let mockBaseApis;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(async function() {
|
||||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||||
|
|
||||||
|
await Olm.init();
|
||||||
|
|
||||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||||
mockBaseApis = {};
|
mockBaseApis = {};
|
||||||
|
|
||||||
@@ -66,7 +69,6 @@ describe("MegolmDecryption", function() {
|
|||||||
describe('receives some keys:', function() {
|
describe('receives some keys:', function() {
|
||||||
let groupSession;
|
let groupSession;
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await Olm.init();
|
|
||||||
groupSession = new global.Olm.OutboundGroupSession();
|
groupSession = new global.Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
@@ -263,5 +265,92 @@ describe("MegolmDecryption", function() {
|
|||||||
// test is successful if no exception is thrown
|
// test is successful if no exception is thrown
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-uses sessions for sequential messages", async function() {
|
||||||
|
const mockStorage = new MockStorageApi();
|
||||||
|
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||||
|
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||||
|
|
||||||
|
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||||
|
olmDevice.verifySignature = expect.createSpy();
|
||||||
|
await olmDevice.init();
|
||||||
|
|
||||||
|
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
|
||||||
|
one_time_keys: {
|
||||||
|
'@alice:home.server': {
|
||||||
|
aliceDevice: {
|
||||||
|
'signed_curve25519:flooble': {
|
||||||
|
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||||
|
signatures: {
|
||||||
|
'@alice:home.server': {
|
||||||
|
'ed25519:aliceDevice': 'totally valid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
|
||||||
|
|
||||||
|
mockCrypto.downloadKeys.andReturn(Promise.resolve({
|
||||||
|
'@alice:home.server': {
|
||||||
|
aliceDevice: {
|
||||||
|
deviceId: 'aliceDevice',
|
||||||
|
isBlocked: expect.createSpy().andReturn(false),
|
||||||
|
isUnverified: expect.createSpy().andReturn(false),
|
||||||
|
getIdentityKey: expect.createSpy().andReturn(
|
||||||
|
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||||
|
),
|
||||||
|
getFingerprint: expect.createSpy().andReturn(''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const megolmEncryption = new MegolmEncryption({
|
||||||
|
userId: '@user:id',
|
||||||
|
crypto: mockCrypto,
|
||||||
|
olmDevice: olmDevice,
|
||||||
|
baseApis: mockBaseApis,
|
||||||
|
roomId: ROOM_ID,
|
||||||
|
config: {
|
||||||
|
rotation_period_ms: 9999999999999,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mockRoom = {
|
||||||
|
getEncryptionTargetMembers: expect.createSpy().andReturn(
|
||||||
|
[{userId: "@alice:home.server"}],
|
||||||
|
),
|
||||||
|
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
|
||||||
|
};
|
||||||
|
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||||
|
body: "Some text",
|
||||||
|
});
|
||||||
|
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// this should have claimed a key for alice as it's starting a new session
|
||||||
|
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||||
|
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||||
|
);
|
||||||
|
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||||
|
['@alice:home.server'], false,
|
||||||
|
);
|
||||||
|
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||||
|
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||||
|
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||||
|
);
|
||||||
|
|
||||||
|
mockBaseApis.claimOneTimeKeys.reset();
|
||||||
|
|
||||||
|
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||||
|
body: "Some more text",
|
||||||
|
});
|
||||||
|
|
||||||
|
// this should *not* have claimed a key as it should be using the same session
|
||||||
|
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
|
||||||
|
|
||||||
|
// likewise they should show the same session ID
|
||||||
|
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -461,6 +461,12 @@ OlmDevice.prototype.createOutboundSession = async function(
|
|||||||
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
||||||
newSessionId = session.session_id();
|
newSessionId = session.session_id();
|
||||||
this._storeAccount(txn, account);
|
this._storeAccount(txn, account);
|
||||||
|
// Pretend we've received a message at this point, otherwise
|
||||||
|
// if we try to send a message to the device, it won't use
|
||||||
|
// this session (storing the creation time separately would
|
||||||
|
// make the pickle longer and would not be useful otherwise).
|
||||||
|
session.set_last_received_message_ts(Date.now());
|
||||||
|
|
||||||
this._saveSession(theirIdentityKey, session, txn);
|
this._saveSession(theirIdentityKey, session, txn);
|
||||||
} finally {
|
} finally {
|
||||||
session.free();
|
session.free();
|
||||||
@@ -725,7 +731,7 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
|
|||||||
*/
|
*/
|
||||||
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||||
const pickled = this._outboundGroupSessionStore[sessionId];
|
const pickled = this._outboundGroupSessionStore[sessionId];
|
||||||
if (pickled === null) {
|
if (pickled === undefined) {
|
||||||
throw new Error("Unknown outbound group session " + sessionId);
|
throw new Error("Unknown outbound group session " + sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,6 +1065,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
|||||||
* @param {string} roomId room in which the message was received
|
* @param {string} roomId room in which the message was received
|
||||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||||
* @param {string} sessionId session identifier
|
* @param {string} sessionId session identifier
|
||||||
|
* @param {integer} chainIndex The chain index at which to export the session.
|
||||||
|
* If omitted, export at the first index we know about.
|
||||||
*
|
*
|
||||||
* @returns {Promise<{chain_index: number, key: string,
|
* @returns {Promise<{chain_index: number, key: string,
|
||||||
* forwarding_curve25519_key_chain: Array<string>,
|
* forwarding_curve25519_key_chain: Array<string>,
|
||||||
@@ -1066,9 +1074,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
|||||||
* }>}
|
* }>}
|
||||||
* details of the session key. The key is a base64-encoded megolm key in
|
* details of the session key. The key is a base64-encoded megolm key in
|
||||||
* export format.
|
* export format.
|
||||||
|
*
|
||||||
|
* @throws Error If the given chain index could not be obtained from the known
|
||||||
|
* index (ie. the given chain index is before the first we have).
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||||
roomId, senderKey, sessionId,
|
roomId, senderKey, sessionId, chainIndex,
|
||||||
) {
|
) {
|
||||||
let result;
|
let result;
|
||||||
await this._cryptoStore.doTxn(
|
await this._cryptoStore.doTxn(
|
||||||
@@ -1079,14 +1090,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
|||||||
result = null;
|
result = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messageIndex = session.first_known_index();
|
|
||||||
|
if (chainIndex === undefined) {
|
||||||
|
chainIndex = session.first_known_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportedSession = session.export_session(chainIndex);
|
||||||
|
|
||||||
const claimedKeys = sessionData.keysClaimed || {};
|
const claimedKeys = sessionData.keysClaimed || {};
|
||||||
const senderEd25519Key = claimedKeys.ed25519 || null;
|
const senderEd25519Key = claimedKeys.ed25519 || null;
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"chain_index": messageIndex,
|
"chain_index": chainIndex,
|
||||||
"key": session.export_session(messageIndex),
|
"key": exportedSession,
|
||||||
"forwarding_curve25519_key_chain":
|
"forwarding_curve25519_key_chain":
|
||||||
sessionData.forwardingCurve25519KeyChain || [],
|
sessionData.forwardingCurve25519KeyChain || [],
|
||||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||||
|
@@ -244,6 +244,21 @@ export default class OutgoingRoomKeyRequestManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for room key requests by target device and state
|
||||||
|
*
|
||||||
|
* @param {string} userId Target user ID
|
||||||
|
* @param {string} deviceId Target device ID
|
||||||
|
*
|
||||||
|
* @return {Promise} resolves to a list of all the
|
||||||
|
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||||
|
*/
|
||||||
|
getOutgoingSentRoomKeyRequest(userId, deviceId) {
|
||||||
|
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||||
|
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// start the background timer to send queued requests, if the timer isn't
|
// start the background timer to send queued requests, if the timer isn't
|
||||||
// already running
|
// already running
|
||||||
_startTimer() {
|
_startTimer() {
|
||||||
|
@@ -144,6 +144,11 @@ function MegolmEncryption(params) {
|
|||||||
// room).
|
// room).
|
||||||
this._setupPromise = Promise.resolve();
|
this._setupPromise = Promise.resolve();
|
||||||
|
|
||||||
|
// Map of outbound sessions by sessions ID. Used if we need a particular
|
||||||
|
// session (the session we're currently using to send is always obtained
|
||||||
|
// using _setupPromise).
|
||||||
|
this._outboundSessions = {};
|
||||||
|
|
||||||
// default rotation periods
|
// default rotation periods
|
||||||
this._sessionRotationPeriodMsgs = 100;
|
this._sessionRotationPeriodMsgs = 100;
|
||||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||||
@@ -195,6 +200,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
logger.log(`Starting new megolm session for room ${self._roomId}`);
|
logger.log(`Starting new megolm session for room ${self._roomId}`);
|
||||||
session = await self._prepareNewSession();
|
session = await self._prepareNewSession();
|
||||||
|
self._outboundSessions[session.sessionId] = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
// now check if we need to share with any devices
|
// now check if we need to share with any devices
|
||||||
@@ -409,8 +415,87 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* Re-shares a megolm session key with devices if the key has already been
|
||||||
|
* sent to them.
|
||||||
*
|
*
|
||||||
|
* @param {string} senderKey The key of the originating device for the session
|
||||||
|
* @param {string} sessionId ID of the outbound session to share
|
||||||
|
* @param {string} userId ID of the user who owns the target device
|
||||||
|
* @param {module:crypto/deviceinfo} device The target device
|
||||||
|
*/
|
||||||
|
MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||||
|
senderKey, sessionId, userId, device,
|
||||||
|
) {
|
||||||
|
const obSessionInfo = this._outboundSessions[sessionId];
|
||||||
|
if (!obSessionInfo) {
|
||||||
|
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chain index of the key we previously sent this device
|
||||||
|
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||||
|
|
||||||
|
// get the key from the inbound session: the outbound one will already
|
||||||
|
// have been ratcheted to the next chain index.
|
||||||
|
const key = await this._olmDevice.getInboundGroupSessionKey(
|
||||||
|
this._roomId, senderKey, sessionId, sentChainIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
logger.warn(
|
||||||
|
"No outbound session key found for " + sessionId + ": not re-sharing keys",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
|
this._olmDevice, this._baseApis, {
|
||||||
|
[userId]: {
|
||||||
|
[device.deviceId]: device,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: "m.forwarded_room_key",
|
||||||
|
content: {
|
||||||
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
|
room_id: this._roomId,
|
||||||
|
session_id: sessionId,
|
||||||
|
session_key: key.key,
|
||||||
|
chain_index: key.chain_index,
|
||||||
|
sender_key: senderKey,
|
||||||
|
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||||
|
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedContent = {
|
||||||
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
|
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||||
|
ciphertext: {},
|
||||||
|
};
|
||||||
|
await olmlib.encryptMessageForDevice(
|
||||||
|
encryptedContent.ciphertext,
|
||||||
|
this._userId,
|
||||||
|
this._deviceId,
|
||||||
|
this._olmDevice,
|
||||||
|
userId,
|
||||||
|
device,
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
|
||||||
|
await this._baseApis.sendToDevice("m.room.encrypted", {
|
||||||
|
[userId]: {
|
||||||
|
[device.deviceId]: encryptedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logger.debug(
|
||||||
|
`Re-shared key for session ${sessionId} with {userId}:{device.deviceId}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||||
*
|
*
|
||||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||||
|
@@ -1133,6 +1133,8 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
|||||||
this._onRoomKeyEvent(event);
|
this._onRoomKeyEvent(event);
|
||||||
} else if (event.getType() == "m.room_key_request") {
|
} else if (event.getType() == "m.room_key_request") {
|
||||||
this._onRoomKeyRequestEvent(event);
|
this._onRoomKeyRequestEvent(event);
|
||||||
|
} else if (event.getContent().msgtype === "m.bad.encrypted") {
|
||||||
|
this._onToDeviceBadEncrypted(event);
|
||||||
} else if (event.isBeingDecrypted()) {
|
} else if (event.isBeingDecrypted()) {
|
||||||
// once the event has been decrypted, try again
|
// once the event has been decrypted, try again
|
||||||
event.once('Event.decrypted', (ev) => {
|
event.once('Event.decrypted', (ev) => {
|
||||||
@@ -1162,6 +1164,74 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
|||||||
alg.onRoomKeyEvent(event);
|
alg.onRoomKeyEvent(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a toDevice event that couldn't be decrypted
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {module:models/event.MatrixEvent} event undecryptable event
|
||||||
|
*/
|
||||||
|
Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
|
||||||
|
const content = event.getWireContent();
|
||||||
|
const sender = event.getSender();
|
||||||
|
const algorithm = content.algorithm;
|
||||||
|
const deviceKey = content.sender_key;
|
||||||
|
|
||||||
|
if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// establish a new olm session with this device since we're failing to decrypt messages
|
||||||
|
// on a current session.
|
||||||
|
// Note that an undecryptable message from another device could easily be spoofed -
|
||||||
|
// is there anything we can do to mitigate this?
|
||||||
|
const device = this._deviceList.getDeviceByIdentityKey(sender, algorithm, deviceKey);
|
||||||
|
const devicesByUser = {};
|
||||||
|
devicesByUser[sender] = [device];
|
||||||
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
|
this._olmDevice, this._baseApis, devicesByUser, true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now send a blank message on that session so the other side knows about it.
|
||||||
|
// (The keyshare request is sent in the clear so that won't do)
|
||||||
|
// We send this first such that, as long as the toDevice messages arrive in the
|
||||||
|
// same order we sent them, the other end will get this first, set up the new session,
|
||||||
|
// then get the keyshare request and send the key over this new session (because it
|
||||||
|
// it the session it has most recently received a message on).
|
||||||
|
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._baseApis.sendToDevice("m.room.encrypted", {
|
||||||
|
[sender]: {
|
||||||
|
[device.deviceId]: encryptedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Most of the time this probably won't be necessary since we'll have queued up a key request when
|
||||||
|
// we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
|
||||||
|
// it. This won't always be the case though so we need to re-send any that have already been sent
|
||||||
|
// to avoid races.
|
||||||
|
const requestsToResend =
|
||||||
|
await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
|
||||||
|
sender, device.deviceId,
|
||||||
|
);
|
||||||
|
for (const keyReq of requestsToResend) {
|
||||||
|
this.cancelRoomKeyRequest(keyReq.requestBody, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a change in the membership state of a member of a room
|
* Handle a change in the membership state of a member of a room
|
||||||
*
|
*
|
||||||
@@ -1287,9 +1357,27 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
|||||||
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
||||||
|
|
||||||
if (userId !== this._userId) {
|
if (userId !== this._userId) {
|
||||||
// TODO: determine if we sent this device the keys already: in
|
if (!this._roomEncryptors[roomId]) {
|
||||||
// which case we can do so again.
|
logger.debug(`room key request for unencrypted room ${roomId}`);
|
||||||
logger.log("Ignoring room key request from other user for now");
|
return;
|
||||||
|
}
|
||||||
|
const encryptor = this._roomEncryptors[roomId];
|
||||||
|
const device = this._deviceList.getStoredDevice(userId, deviceId);
|
||||||
|
if (!device) {
|
||||||
|
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await encryptor.reshareKeyWithDevice(
|
||||||
|
body.sender_key, body.session_id, userId, device,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
"Failed to re-share keys for session " + body.session_id +
|
||||||
|
" with device " + userId + ":" + device.deviceId, e,
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -116,14 +116,17 @@ module.exports.encryptMessageForDevice = async function(
|
|||||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||||
*
|
*
|
||||||
* @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 to ensure sessions for
|
||||||
|
*
|
||||||
|
* @param {bolean} force If true, establish a new session even if one already exists.
|
||||||
|
* Optional.
|
||||||
*
|
*
|
||||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||||
* an Object mapping from userId to deviceId to
|
* an Object mapping from userId to deviceId to
|
||||||
* {@link module:crypto~OlmSessionResult}
|
* {@link module:crypto~OlmSessionResult}
|
||||||
*/
|
*/
|
||||||
module.exports.ensureOlmSessionsForDevices = async function(
|
module.exports.ensureOlmSessionsForDevices = async function(
|
||||||
olmDevice, baseApis, devicesByUser,
|
olmDevice, baseApis, devicesByUser, force,
|
||||||
) {
|
) {
|
||||||
const devicesWithoutSession = [
|
const devicesWithoutSession = [
|
||||||
// [userId, deviceId], ...
|
// [userId, deviceId], ...
|
||||||
@@ -141,7 +144,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
|||||||
const deviceId = deviceInfo.deviceId;
|
const deviceId = deviceInfo.deviceId;
|
||||||
const key = deviceInfo.getIdentityKey();
|
const key = deviceInfo.getIdentityKey();
|
||||||
const sessionId = await olmDevice.getSessionIdForDevice(key);
|
const sessionId = await olmDevice.getSessionIdForDevice(key);
|
||||||
if (sessionId === null) {
|
if (sessionId === null || force) {
|
||||||
devicesWithoutSession.push([userId, deviceId]);
|
devicesWithoutSession.push([userId, deviceId]);
|
||||||
}
|
}
|
||||||
result[userId][deviceId] = {
|
result[userId][deviceId] = {
|
||||||
@@ -177,7 +180,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
|||||||
for (let j = 0; j < devices.length; j++) {
|
for (let j = 0; j < devices.length; j++) {
|
||||||
const deviceInfo = devices[j];
|
const deviceInfo = devices[j];
|
||||||
const deviceId = deviceInfo.deviceId;
|
const deviceId = deviceInfo.deviceId;
|
||||||
if (result[userId][deviceId].sessionId) {
|
if (result[userId][deviceId].sessionId && !force) {
|
||||||
// we already have a result for this device
|
// we already have a result for this device
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@@ -206,6 +206,42 @@ export class Backend {
|
|||||||
return promiseifyTxn(txn).then(() => result);
|
return promiseifyTxn(txn).then(() => result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||||
|
let stateIndex = 0;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
function onsuccess(ev) {
|
||||||
|
const cursor = ev.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
const keyReq = cursor.value;
|
||||||
|
if (keyReq.recipients.includes({userId, deviceId})) {
|
||||||
|
results.push(keyReq);
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
// try the next state in the list
|
||||||
|
stateIndex++;
|
||||||
|
if (stateIndex >= wantedStates.length) {
|
||||||
|
// no matches
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantedState = wantedStates[stateIndex];
|
||||||
|
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||||
|
cursorReq.onsuccess = onsuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||||
|
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||||
|
|
||||||
|
const wantedState = wantedStates[stateIndex];
|
||||||
|
const cursorReq = store.index("state").openCursor(wantedState);
|
||||||
|
cursorReq.onsuccess = onsuccess;
|
||||||
|
|
||||||
|
return promiseifyTxn(txn).then(() => results);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for an existing room key request by id and state, and update it if
|
* Look for an existing room key request by id and state, and update it if
|
||||||
* found
|
* found
|
||||||
|
@@ -207,6 +207,24 @@ export default class IndexedDBCryptoStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for room key requests by target device and state
|
||||||
|
*
|
||||||
|
* @param {string} userId Target user ID
|
||||||
|
* @param {string} deviceId Target device ID
|
||||||
|
* @param {Array<Number>} wantedStates list of acceptable states
|
||||||
|
*
|
||||||
|
* @return {Promise} resolves to a list of all the
|
||||||
|
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||||
|
*/
|
||||||
|
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||||
|
return this._connect().then((backend) => {
|
||||||
|
return backend.getOutgoingRoomKeyRequestsByTarget(
|
||||||
|
userId, deviceId, wantedStates,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for an existing room key request by id and state, and update it if
|
* Look for an existing room key request by id and state, and update it if
|
||||||
* found
|
* found
|
||||||
|
@@ -145,6 +145,19 @@ export default class MemoryCryptoStore {
|
|||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const req of this._outgoingRoomKeyRequests) {
|
||||||
|
for (const state of wantedStates) {
|
||||||
|
if (req.state === state && req.recipients.includes({userId, deviceId})) {
|
||||||
|
results.push(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(results);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for an existing room key request by id and state, and update it if
|
* Look for an existing room key request by id and state, and update it if
|
||||||
* found
|
* found
|
||||||
|
Reference in New Issue
Block a user