Skip to content

Commit 550086e

Browse files
authored
Merge pull request #850 from uhoreg/ensure_key_request
make sure key requests get sent
2 parents 5308595 + 055ce67 commit 550086e

File tree

6 files changed

+165
-53
lines changed

6 files changed

+165
-53
lines changed

spec/unit/crypto.spec.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ describe("Crypto", function() {
139139
await bobClient.initCrypto();
140140
});
141141

142+
afterEach(async function() {
143+
aliceClient.stopClient();
144+
bobClient.stopClient();
145+
});
146+
142147
it(
143148
"does not cancel keyshare requests if some messages are not decrypted",
144149
async function() {
@@ -266,10 +271,31 @@ describe("Crypto", function() {
266271
// the room key request should be gone since we've now decypted everything
267272
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
268273
.toNotExist();
269-
270-
aliceClient.stopClient();
271-
bobClient.stopClient();
272274
},
273275
);
276+
277+
it("creates a new keyshare request if we request a keyshare", async function() {
278+
// make sure that cancelAndResend... creates a new keyshare request
279+
// if there wasn't an already-existing one
280+
const event = new MatrixEvent({
281+
sender: "@bob:example.com",
282+
room_id: "!someroom",
283+
content: {
284+
algorithm: olmlib.MEGOLM_ALGORITHM,
285+
session_id: "sessionid",
286+
sender_key: "senderkey",
287+
},
288+
});
289+
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
290+
const cryptoStore = aliceClient._cryptoStore;
291+
const roomKeyRequestBody = {
292+
algorithm: olmlib.MEGOLM_ALGORITHM,
293+
room_id: "!someroom",
294+
session_id: "sessionid",
295+
sender_key: "senderkey",
296+
};
297+
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
298+
.toExist();
299+
});
274300
});
275301
});

src/client.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,9 +760,10 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
760760
* request.
761761
* @param {MatrixEvent} event event of which to cancel and resend the room
762762
* key request.
763+
* @return {Promise} A promise that will resolve when the key request is queued
763764
*/
764765
MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
765-
event.cancelAndResendKeyRequest(this._crypto);
766+
return event.cancelAndResendKeyRequest(this._crypto, this.getUserId());
766767
};
767768

768769
/**

src/crypto/OutgoingRoomKeyRequestManager.js

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -124,35 +124,115 @@ export default class OutgoingRoomKeyRequestManager {
124124
*
125125
* @param {module:crypto~RoomKeyRequestBody} requestBody
126126
* @param {Array<{userId: string, deviceId: string}>} recipients
127+
* @param {boolean} resend whether to resend the key request if there is
128+
* already one
127129
*
128130
* @returns {Promise} resolves when the request has been added to the
129131
* pending list (or we have established that a similar request already
130132
* exists)
131133
*/
132-
sendRoomKeyRequest(requestBody, recipients) {
133-
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
134-
requestBody: requestBody,
135-
recipients: recipients,
136-
requestId: this._baseApis.makeTxnId(),
137-
state: ROOM_KEY_REQUEST_STATES.UNSENT,
138-
}).then((req) => {
139-
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
140-
this._startTimer();
134+
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
135+
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
136+
requestBody,
137+
);
138+
if (!req) {
139+
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
140+
requestBody: requestBody,
141+
recipients: recipients,
142+
requestId: this._baseApis.makeTxnId(),
143+
state: ROOM_KEY_REQUEST_STATES.UNSENT,
144+
});
145+
} else {
146+
switch (req.state) {
147+
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
148+
case ROOM_KEY_REQUEST_STATES.UNSENT:
149+
// nothing to do here, since we're going to send a request anyways
150+
return;
151+
152+
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
153+
// existing request is about to be cancelled. If we want to
154+
// resend, then change the state so that it resends after
155+
// cancelling. Otherwise, just cancel the cancellation.
156+
const state = resend ?
157+
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
158+
ROOM_KEY_REQUEST_STATES.SENT;
159+
await this._cryptoStore.updateOutgoingRoomKeyRequest(
160+
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
161+
state,
162+
cancellationTxnId: this._baseApis.makeTxnId(),
163+
},
164+
);
165+
break;
141166
}
142-
});
167+
case ROOM_KEY_REQUEST_STATES.SENT: {
168+
// a request has already been sent. If we don't want to
169+
// resend, then do nothing. If we do want to, then cancel the
170+
// existing request and send a new one.
171+
if (resend) {
172+
const state =
173+
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILLRESEND;
174+
const updatedReq =
175+
await this._cryptoStore.updateOutgoingRoomKeyRequest(
176+
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
177+
state,
178+
cancellationTxnId: this._baseApis.makeTxnId(),
179+
},
180+
);
181+
if (!updatedReq) {
182+
// updateOutgoingRoomKeyRequest couldn't find the request
183+
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
184+
// raced with another tab to mark the request cancelled.
185+
// Try again, to make sure the request is resent.
186+
return await this.sendRoomKeyRequest(
187+
requestBody, recipients, resend,
188+
);
189+
}
190+
191+
// We don't want to wait for the timer, so we send it
192+
// immediately. (We might actually end up racing with the timer,
193+
// but that's ok: even if we make the request twice, we'll do it
194+
// with the same transaction_id, so only one message will get
195+
// sent).
196+
//
197+
// (We also don't want to wait for the response from the server
198+
// here, as it will slow down processing of received keys if we
199+
// do.)
200+
try {
201+
await this._sendOutgoingRoomKeyRequestCancellation(
202+
updatedReq,
203+
true,
204+
);
205+
} catch (e) {
206+
logger.error(
207+
"Error sending room key request cancellation;"
208+
+ " will retry later.", e,
209+
);
210+
}
211+
// The request has transitioned from
212+
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
213+
// still need to resend the request which is now UNSENT, so
214+
// start the timer if it isn't already started.
215+
}
216+
break;
217+
}
218+
default:
219+
throw new Error('unhandled state: ' + req.state);
220+
}
221+
}
222+
// some of the branches require the timer to be started. Just start it
223+
// all the time, because it doesn't hurt to start it.
224+
this._startTimer();
143225
}
144226

145227
/**
146228
* Cancel room key requests, if any match the given requestBody
147229
*
148230
* @param {module:crypto~RoomKeyRequestBody} requestBody
149-
* @param {boolean} andResend if true, transition to UNSENT instead of
150-
* deleting after sending cancellation.
151231
*
152232
* @returns {Promise} resolves when the request has been updated in our
153233
* pending list.
154234
*/
155-
cancelRoomKeyRequest(requestBody, andResend=false) {
235+
cancelRoomKeyRequest(requestBody) {
156236
return this._cryptoStore.getOutgoingRoomKeyRequest(
157237
requestBody,
158238
).then((req) => {
@@ -183,15 +263,10 @@ export default class OutgoingRoomKeyRequestManager {
183263
);
184264

185265
case ROOM_KEY_REQUEST_STATES.SENT: {
186-
// If `andResend` is set, transition to UNSENT once the
187-
// cancellation has successfully been sent.
188-
const state = andResend ?
189-
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
190-
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING;
191266
// send a cancellation.
192267
return this._cryptoStore.updateOutgoingRoomKeyRequest(
193268
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
194-
state,
269+
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
195270
cancellationTxnId: this._baseApis.makeTxnId(),
196271
},
197272
).then((updatedReq) => {
@@ -221,20 +296,12 @@ export default class OutgoingRoomKeyRequestManager {
221296
// do.)
222297
this._sendOutgoingRoomKeyRequestCancellation(
223298
updatedReq,
224-
andResend,
225299
).catch((e) => {
226300
logger.error(
227301
"Error sending room key request cancellation;"
228302
+ " will retry later.", e,
229303
);
230304
this._startTimer();
231-
}).then(() => {
232-
if (!andResend) return;
233-
// The request has transitioned from
234-
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
235-
// still need to resend the request which is now UNSENT, so
236-
// start the timer if it isn't already started.
237-
this._startTimer();
238305
});
239306
});
240307
}

src/crypto/algorithms/megolm.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -815,19 +815,9 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
815815
};
816816

817817
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
818-
const sender = event.getSender();
819818
const wireContent = event.getWireContent();
820819

821-
// send the request to all of our own devices, and the
822-
// original sending device if it wasn't us.
823-
const recipients = [{
824-
userId: this._userId, deviceId: '*',
825-
}];
826-
if (sender != this._userId) {
827-
recipients.push({
828-
userId: sender, deviceId: wireContent.device_id,
829-
});
830-
}
820+
const recipients = event.getKeyRequestRecipients(this._userId);
831821

832822
this._crypto.requestRoomKey({
833823
room_id: event.getRoomId(),

src/crypto/index.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,10 +1426,14 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi
14261426
*
14271427
* @param {module:crypto~RoomKeyRequestBody} requestBody
14281428
* @param {Array<{userId: string, deviceId: string}>} recipients
1429+
* @param {boolean} resend whether to resend the key request if there is
1430+
* already one
1431+
*
1432+
* @return {Promise} a promise that resolves when the key request is queued
14291433
*/
1430-
Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
1431-
this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
1432-
requestBody, recipients,
1434+
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
1435+
return this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
1436+
requestBody, recipients, resend,
14331437
).catch((e) => {
14341438
// this normally means we couldn't talk to the store
14351439
logger.error(
@@ -1443,11 +1447,9 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
14431447
*
14441448
* @param {module:crypto~RoomKeyRequestBody} requestBody
14451449
* parameters to match for cancellation
1446-
* @param {boolean} andResend
1447-
* if true, resend the key request after cancelling.
14481450
*/
1449-
Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) {
1450-
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend)
1451+
Crypto.prototype.cancelRoomKeyRequest = function(requestBody) {
1452+
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
14511453
.catch((e) => {
14521454
logger.warn("Error clearing pending room key requests", e);
14531455
}).done();
@@ -1984,7 +1986,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
19841986
sender, device.deviceId,
19851987
);
19861988
for (const keyReq of requestsToResend) {
1987-
this.cancelRoomKeyRequest(keyReq.requestBody, true);
1989+
this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
19881990
}
19891991
};
19901992

src/models/event.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,15 +382,41 @@ utils.extend(module.exports.MatrixEvent.prototype, {
382382
* Cancel any room key request for this event and resend another.
383383
*
384384
* @param {module:crypto} crypto crypto module
385+
* @param {string} userId the user who received this event
386+
*
387+
* @returns {Promise} a promise that resolves when the request is queued
385388
*/
386-
cancelAndResendKeyRequest: function(crypto) {
389+
cancelAndResendKeyRequest: function(crypto, userId) {
387390
const wireContent = this.getWireContent();
388-
crypto.cancelRoomKeyRequest({
391+
return crypto.requestRoomKey({
389392
algorithm: wireContent.algorithm,
390393
room_id: this.getRoomId(),
391394
session_id: wireContent.session_id,
392395
sender_key: wireContent.sender_key,
393-
}, true);
396+
}, this.getKeyRequestRecipients(userId), true);
397+
},
398+
399+
/**
400+
* Calculate the recipients for keyshare requests.
401+
*
402+
* @param {string} userId the user who received this event.
403+
*
404+
* @returns {Array} array of recipients
405+
*/
406+
getKeyRequestRecipients: function(userId) {
407+
// send the request to all of our own devices, and the
408+
// original sending device if it wasn't us.
409+
const wireContent = this.getWireContent();
410+
const recipients = [{
411+
userId, deviceId: '*',
412+
}];
413+
const sender = this.getSender();
414+
if (sender !== userId) {
415+
recipients.push({
416+
userId: sender, deviceId: wireContent.device_id,
417+
});
418+
}
419+
return recipients;
394420
},
395421

396422
_decryptionLoop: async function(crypto) {

0 commit comments

Comments
 (0)