UNPKG

35 kBJavaScriptView Raw
1/*
2Copyright 2016 OpenMarket Ltd
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17"use strict";
18
19const anotherjson = require('another-json');
20import Promise from 'bluebird';
21import expect from 'expect';
22
23const utils = require('../../lib/utils');
24const testUtils = require('../test-utils');
25const TestClient = require('../TestClient').default;
26import logger from '../../src/logger';
27
28const ROOM_ID = "!room:id";
29
30/**
31 * start an Olm session with a given recipient
32 *
33 * @param {Olm.Account} olmAccount
34 * @param {TestClient} recipientTestClient
35 * @return {Promise} promise for Olm.Session
36 */
37function createOlmSession(olmAccount, recipientTestClient) {
38 return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
39 const otkId = utils.keys(keys)[0];
40 const otk = keys[otkId];
41
42 const session = new global.Olm.Session();
43 session.create_outbound(
44 olmAccount, recipientTestClient.getDeviceKey(), otk.key,
45 );
46 return session;
47 });
48}
49
50/**
51 * encrypt an event with olm
52 *
53 * @param {object} opts
54 * @param {string=} opts.sender
55 * @param {string} opts.senderKey
56 * @param {Olm.Session} opts.p2pSession
57 * @param {TestClient} opts.recipient
58 * @param {object=} opts.plaincontent
59 * @param {string=} opts.plaintype
60 *
61 * @return {object} event
62 */
63function encryptOlmEvent(opts) {
64 expect(opts.senderKey).toBeTruthy();
65 expect(opts.p2pSession).toBeTruthy();
66 expect(opts.recipient).toBeTruthy();
67
68 const plaintext = {
69 content: opts.plaincontent || {},
70 recipient: opts.recipient.userId,
71 recipient_keys: {
72 ed25519: opts.recipient.getSigningKey(),
73 },
74 sender: opts.sender || '@bob:xyz',
75 type: opts.plaintype || 'm.test',
76 };
77
78 const event = {
79 content: {
80 algorithm: 'm.olm.v1.curve25519-aes-sha2',
81 ciphertext: {},
82 sender_key: opts.senderKey,
83 },
84 sender: opts.sender || '@bob:xyz',
85 type: 'm.room.encrypted',
86 };
87 event.content.ciphertext[opts.recipient.getDeviceKey()] =
88 opts.p2pSession.encrypt(JSON.stringify(plaintext));
89 return event;
90}
91
92/**
93 * encrypt an event with megolm
94 *
95 * @param {object} opts
96 * @param {string} opts.senderKey
97 * @param {Olm.OutboundGroupSession} opts.groupSession
98 * @param {object=} opts.plaintext
99 * @param {string=} opts.room_id
100 *
101 * @return {object} event
102 */
103function encryptMegolmEvent(opts) {
104 expect(opts.senderKey).toBeTruthy();
105 expect(opts.groupSession).toBeTruthy();
106
107 const plaintext = opts.plaintext || {};
108 if (!plaintext.content) {
109 plaintext.content = {
110 body: '42',
111 msgtype: "m.text",
112 };
113 }
114 if (!plaintext.type) {
115 plaintext.type = "m.room.message";
116 }
117 if (!plaintext.room_id) {
118 expect(opts.room_id).toBeTruthy();
119 plaintext.room_id = opts.room_id;
120 }
121
122 return {
123 event_id: 'test_megolm_event',
124 content: {
125 algorithm: "m.megolm.v1.aes-sha2",
126 ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
127 device_id: "testDevice",
128 sender_key: opts.senderKey,
129 session_id: opts.groupSession.session_id(),
130 },
131 type: "m.room.encrypted",
132 };
133}
134
135/**
136 * build an encrypted room_key event to share a group session
137 *
138 * @param {object} opts
139 * @param {string} opts.senderKey
140 * @param {TestClient} opts.recipient
141 * @param {Olm.Session} opts.p2pSession
142 * @param {Olm.OutboundGroupSession} opts.groupSession
143 * @param {string=} opts.room_id
144 *
145 * @return {object} event
146 */
147function encryptGroupSessionKey(opts) {
148 return encryptOlmEvent({
149 senderKey: opts.senderKey,
150 recipient: opts.recipient,
151 p2pSession: opts.p2pSession,
152 plaincontent: {
153 algorithm: 'm.megolm.v1.aes-sha2',
154 room_id: opts.room_id,
155 session_id: opts.groupSession.session_id(),
156 session_key: opts.groupSession.session_key(),
157 },
158 plaintype: 'm.room_key',
159 });
160}
161
162/**
163 * get a /sync response which contains a single room (ROOM_ID),
164 * with the members given
165 *
166 * @param {string[]} roomMembers
167 *
168 * @return {object} event
169 */
170function getSyncResponse(roomMembers) {
171 const roomResponse = {
172 state: {
173 events: [
174 testUtils.mkEvent({
175 type: 'm.room.encryption',
176 skey: '',
177 content: {
178 algorithm: 'm.megolm.v1.aes-sha2',
179 },
180 }),
181 ],
182 },
183 };
184
185 for (let i = 0; i < roomMembers.length; i++) {
186 roomResponse.state.events.push(
187 testUtils.mkMembership({
188 mship: 'join',
189 sender: roomMembers[i],
190 }),
191 );
192 }
193
194 const syncResponse = {
195 next_batch: 1,
196 rooms: {
197 join: {},
198 },
199 };
200 syncResponse.rooms.join[ROOM_ID] = roomResponse;
201 return syncResponse;
202}
203
204
205describe("megolm", function() {
206 if (!global.Olm) {
207 logger.warn('not running megolm tests: Olm not present');
208 return;
209 }
210 const Olm = global.Olm;
211
212 let testOlmAccount;
213 let testSenderKey;
214 let aliceTestClient;
215
216 /**
217 * Get the device keys for testOlmAccount in a format suitable for a
218 * response to /keys/query
219 *
220 * @param {string} userId The user ID to query for
221 * @returns {Object} The fake query response
222 */
223 function getTestKeysQueryResponse(userId) {
224 const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
225 const testDeviceKeys = {
226 algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
227 device_id: 'DEVICE_ID',
228 keys: {
229 'curve25519:DEVICE_ID': testE2eKeys.curve25519,
230 'ed25519:DEVICE_ID': testE2eKeys.ed25519,
231 },
232 user_id: userId,
233 };
234 const j = anotherjson.stringify(testDeviceKeys);
235 const sig = testOlmAccount.sign(j);
236 testDeviceKeys.signatures = {};
237 testDeviceKeys.signatures[userId] = {
238 'ed25519:DEVICE_ID': sig,
239 };
240
241 const queryResponse = {
242 device_keys: {},
243 };
244
245 queryResponse.device_keys[userId] = {
246 'DEVICE_ID': testDeviceKeys,
247 };
248
249 return queryResponse;
250 }
251
252 /**
253 * Get a one-time key for testOlmAccount in a format suitable for a
254 * response to /keys/claim
255
256 * @param {string} userId The user ID to query for
257 * @returns {Object} The fake key claim response
258 */
259 function getTestKeysClaimResponse(userId) {
260 testOlmAccount.generate_one_time_keys(1);
261 const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
262 testOlmAccount.mark_keys_as_published();
263
264 const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
265 const oneTimeKey = testOneTimeKeys.curve25519[keyId];
266 const keyResult = {
267 'key': oneTimeKey,
268 };
269 const j = anotherjson.stringify(keyResult);
270 const sig = testOlmAccount.sign(j);
271 keyResult.signatures = {};
272 keyResult.signatures[userId] = {
273 'ed25519:DEVICE_ID': sig,
274 };
275
276 const claimResponse = {one_time_keys: {}};
277 claimResponse.one_time_keys[userId] = {
278 'DEVICE_ID': {},
279 };
280 claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
281 keyResult;
282 return claimResponse;
283 }
284
285 beforeEach(async function() {
286 testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
287
288 aliceTestClient = new TestClient(
289 "@alice:localhost", "xzcvb", "akjgkrgjs",
290 );
291 await aliceTestClient.client.initCrypto();
292
293 testOlmAccount = new Olm.Account();
294 testOlmAccount.create();
295 const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
296 testSenderKey = testE2eKeys.curve25519;
297 });
298
299 afterEach(function() {
300 return aliceTestClient.stop();
301 });
302
303 it("Alice receives a megolm message", function() {
304 return aliceTestClient.start().then(() => {
305 return createOlmSession(testOlmAccount, aliceTestClient);
306 }).then((p2pSession) => {
307 const groupSession = new Olm.OutboundGroupSession();
308 groupSession.create();
309
310 // make the room_key event
311 const roomKeyEncrypted = encryptGroupSessionKey({
312 senderKey: testSenderKey,
313 recipient: aliceTestClient,
314 p2pSession: p2pSession,
315 groupSession: groupSession,
316 room_id: ROOM_ID,
317 });
318
319 // encrypt a message with the group session
320 const messageEncrypted = encryptMegolmEvent({
321 senderKey: testSenderKey,
322 groupSession: groupSession,
323 room_id: ROOM_ID,
324 });
325
326 // Alice gets both the events in a single sync
327 const syncResponse = {
328 next_batch: 1,
329 to_device: {
330 events: [roomKeyEncrypted],
331 },
332 rooms: {
333 join: {},
334 },
335 };
336 syncResponse.rooms.join[ROOM_ID] = {
337 timeline: {
338 events: [messageEncrypted],
339 },
340 };
341
342 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
343 return aliceTestClient.flushSync();
344 }).then(function() {
345 const room = aliceTestClient.client.getRoom(ROOM_ID);
346 const event = room.getLiveTimeline().getEvents()[0];
347 expect(event.isEncrypted()).toBe(true);
348 return testUtils.awaitDecryption(event);
349 }).then((event) => {
350 expect(event.getContent().body).toEqual('42');
351 });
352 });
353
354 it("Alice receives a megolm message before the session keys", function() {
355 // https://github.com/vector-im/riot-web/issues/2273
356 let roomKeyEncrypted;
357
358 return aliceTestClient.start().then(() => {
359 return createOlmSession(testOlmAccount, aliceTestClient);
360 }).then((p2pSession) => {
361 const groupSession = new Olm.OutboundGroupSession();
362 groupSession.create();
363
364 // make the room_key event, but don't send it yet
365 roomKeyEncrypted = encryptGroupSessionKey({
366 senderKey: testSenderKey,
367 recipient: aliceTestClient,
368 p2pSession: p2pSession,
369 groupSession: groupSession,
370 room_id: ROOM_ID,
371 });
372
373 // encrypt a message with the group session
374 const messageEncrypted = encryptMegolmEvent({
375 senderKey: testSenderKey,
376 groupSession: groupSession,
377 room_id: ROOM_ID,
378 });
379
380 // Alice just gets the message event to start with
381 const syncResponse = {
382 next_batch: 1,
383 rooms: {
384 join: {},
385 },
386 };
387 syncResponse.rooms.join[ROOM_ID] = {
388 timeline: {
389 events: [messageEncrypted],
390 },
391 };
392
393 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
394 return aliceTestClient.flushSync();
395 }).then(function() {
396 const room = aliceTestClient.client.getRoom(ROOM_ID);
397 const event = room.getLiveTimeline().getEvents()[0];
398 expect(event.getContent().msgtype).toEqual('m.bad.encrypted');
399
400 // now she gets the room_key event
401 const syncResponse = {
402 next_batch: 2,
403 to_device: {
404 events: [roomKeyEncrypted],
405 },
406 };
407
408 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
409 return aliceTestClient.flushSync();
410 }).then(function() {
411 const room = aliceTestClient.client.getRoom(ROOM_ID);
412 const event = room.getLiveTimeline().getEvents()[0];
413
414 if (event.getContent().msgtype != 'm.bad.encrypted') {
415 return event;
416 }
417
418 return new Promise((resolve, reject) => {
419 event.once('Event.decrypted', (ev) => {
420 logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
421 resolve(ev);
422 });
423 });
424 }).then((event) => {
425 expect(event.getContent().body).toEqual('42');
426 });
427 });
428
429 it("Alice gets a second room_key message", function() {
430 return aliceTestClient.start().then(() => {
431 return createOlmSession(testOlmAccount, aliceTestClient);
432 }).then((p2pSession) => {
433 const groupSession = new Olm.OutboundGroupSession();
434 groupSession.create();
435
436 // make the room_key event
437 const roomKeyEncrypted1 = encryptGroupSessionKey({
438 senderKey: testSenderKey,
439 recipient: aliceTestClient,
440 p2pSession: p2pSession,
441 groupSession: groupSession,
442 room_id: ROOM_ID,
443 });
444
445 // encrypt a message with the group session
446 const messageEncrypted = encryptMegolmEvent({
447 senderKey: testSenderKey,
448 groupSession: groupSession,
449 room_id: ROOM_ID,
450 });
451
452 // make a second room_key event now that we have advanced the group
453 // session.
454 const roomKeyEncrypted2 = encryptGroupSessionKey({
455 senderKey: testSenderKey,
456 recipient: aliceTestClient,
457 p2pSession: p2pSession,
458 groupSession: groupSession,
459 room_id: ROOM_ID,
460 });
461
462 // on the first sync, send the best room key
463 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
464 next_batch: 1,
465 to_device: {
466 events: [roomKeyEncrypted1],
467 },
468 });
469
470 // on the second sync, send the advanced room key, along with the
471 // message. This simulates the situation where Alice has been sent a
472 // later copy of the room key and is reloading the client.
473 const syncResponse2 = {
474 next_batch: 2,
475 to_device: {
476 events: [roomKeyEncrypted2],
477 },
478 rooms: {
479 join: {},
480 },
481 };
482 syncResponse2.rooms.join[ROOM_ID] = {
483 timeline: {
484 events: [messageEncrypted],
485 },
486 };
487 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
488
489 // flush both syncs
490 return aliceTestClient.flushSync().then(() => {
491 return aliceTestClient.flushSync();
492 });
493 }).then(function() {
494 const room = aliceTestClient.client.getRoom(ROOM_ID);
495 const event = room.getLiveTimeline().getEvents()[0];
496 expect(event.getContent().body).toEqual('42');
497 });
498 });
499
500 it('Alice sends a megolm message', function() {
501 let p2pSession;
502
503 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
504 return aliceTestClient.start().then(() => {
505 // establish an olm session with alice
506 return createOlmSession(testOlmAccount, aliceTestClient);
507 }).then((_p2pSession) => {
508 p2pSession = _p2pSession;
509
510 const syncResponse = getSyncResponse(['@bob:xyz']);
511
512 const olmEvent = encryptOlmEvent({
513 senderKey: testSenderKey,
514 recipient: aliceTestClient,
515 p2pSession: p2pSession,
516 });
517
518 syncResponse.to_device = { events: [olmEvent] };
519
520 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
521 return aliceTestClient.flushSync();
522 }).then(function() {
523 // start out with the device unknown - the send should be rejected.
524 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
525 200, getTestKeysQueryResponse('@bob:xyz'),
526 );
527
528 return Promise.all([
529 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
530 throw new Error("sendTextMessage failed on an unknown device");
531 }, (e) => {
532 expect(e.name).toEqual("UnknownDeviceError");
533 }),
534 aliceTestClient.httpBackend.flushAllExpected(),
535 ]);
536 }).then(function() {
537 // mark the device as known, and resend.
538 aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
539
540 let inboundGroupSession;
541 aliceTestClient.httpBackend.when(
542 'PUT', '/sendToDevice/m.room.encrypted/',
543 ).respond(200, function(path, content) {
544 const m = content.messages['@bob:xyz'].DEVICE_ID;
545 const ct = m.ciphertext[testSenderKey];
546 const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
547
548 expect(decrypted.type).toEqual('m.room_key');
549 inboundGroupSession = new Olm.InboundGroupSession();
550 inboundGroupSession.create(decrypted.content.session_key);
551 return {};
552 });
553
554 aliceTestClient.httpBackend.when(
555 'PUT', '/send/',
556 ).respond(200, function(path, content) {
557 const ct = content.ciphertext;
558 const r = inboundGroupSession.decrypt(ct);
559 logger.log('Decrypted received megolm message', r);
560
561 expect(r.message_index).toEqual(0);
562 const decrypted = JSON.parse(r.plaintext);
563 expect(decrypted.type).toEqual('m.room.message');
564 expect(decrypted.content.body).toEqual('test');
565
566 return {
567 event_id: '$event_id',
568 };
569 });
570
571 const room = aliceTestClient.client.getRoom(ROOM_ID);
572 const pendingMsg = room.getPendingEvents()[0];
573
574 return Promise.all([
575 aliceTestClient.client.resendEvent(pendingMsg, room),
576
577 // the crypto stuff can take a while, so give the requests a whole second.
578 aliceTestClient.httpBackend.flushAllExpected({
579 timeout: 1000,
580 }),
581 ]);
582 });
583 });
584
585 it("We shouldn't attempt to send to blocked devices", function() {
586 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
587 return aliceTestClient.start().then(() => {
588 // establish an olm session with alice
589 return createOlmSession(testOlmAccount, aliceTestClient);
590 }).then((p2pSession) => {
591 const syncResponse = getSyncResponse(['@bob:xyz']);
592
593 const olmEvent = encryptOlmEvent({
594 senderKey: testSenderKey,
595 recipient: aliceTestClient,
596 p2pSession: p2pSession,
597 });
598
599 syncResponse.to_device = { events: [olmEvent] };
600 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
601
602 return aliceTestClient.flushSync();
603 }).then(function() {
604 logger.log('Forcing alice to download our device keys');
605
606 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
607 200, getTestKeysQueryResponse('@bob:xyz'),
608 );
609
610 return Promise.all([
611 aliceTestClient.client.downloadKeys(['@bob:xyz']),
612 aliceTestClient.httpBackend.flush('/keys/query', 1),
613 ]);
614 }).then(function() {
615 logger.log('Telling alice to block our device');
616 aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
617
618 logger.log('Telling alice to send a megolm message');
619 aliceTestClient.httpBackend.when(
620 'PUT', '/send/',
621 ).respond(200, {
622 event_id: '$event_id',
623 });
624
625 return Promise.all([
626 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
627
628 // the crypto stuff can take a while, so give the requests a whole second.
629 aliceTestClient.httpBackend.flushAllExpected({
630 timeout: 1000,
631 }),
632 ]);
633 });
634 });
635
636 it("We should start a new megolm session when a device is blocked", function() {
637 let p2pSession;
638 let megolmSessionId;
639
640 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
641 return aliceTestClient.start().then(() => {
642 // establish an olm session with alice
643 return createOlmSession(testOlmAccount, aliceTestClient);
644 }).then((_p2pSession) => {
645 p2pSession = _p2pSession;
646
647 const syncResponse = getSyncResponse(['@bob:xyz']);
648
649 const olmEvent = encryptOlmEvent({
650 senderKey: testSenderKey,
651 recipient: aliceTestClient,
652 p2pSession: p2pSession,
653 });
654
655 syncResponse.to_device = { events: [olmEvent] };
656 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
657
658 return aliceTestClient.flushSync();
659 }).then(function() {
660 logger.log("Fetching bob's devices and marking known");
661
662 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
663 200, getTestKeysQueryResponse('@bob:xyz'),
664 );
665
666 return Promise.all([
667 aliceTestClient.client.downloadKeys(['@bob:xyz']),
668 aliceTestClient.httpBackend.flushAllExpected(),
669 ]).then((keys) => {
670 aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
671 });
672 }).then(function() {
673 logger.log('Telling alice to send a megolm message');
674
675 aliceTestClient.httpBackend.when(
676 'PUT', '/sendToDevice/m.room.encrypted/',
677 ).respond(200, function(path, content) {
678 logger.log('sendToDevice: ', content);
679 const m = content.messages['@bob:xyz'].DEVICE_ID;
680 const ct = m.ciphertext[testSenderKey];
681 expect(ct.type).toEqual(1); // normal message
682 const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
683 logger.log('decrypted sendToDevice:', decrypted);
684 expect(decrypted.type).toEqual('m.room_key');
685 megolmSessionId = decrypted.content.session_id;
686 return {};
687 });
688
689 aliceTestClient.httpBackend.when(
690 'PUT', '/send/',
691 ).respond(200, function(path, content) {
692 logger.log('/send:', content);
693 expect(content.session_id).toEqual(megolmSessionId);
694 return {
695 event_id: '$event_id',
696 };
697 });
698
699 return Promise.all([
700 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
701
702 // the crypto stuff can take a while, so give the requests a whole second.
703 aliceTestClient.httpBackend.flushAllExpected({
704 timeout: 1000,
705 }),
706 ]);
707 }).then(function() {
708 logger.log('Telling alice to block our device');
709 aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
710
711 logger.log('Telling alice to send another megolm message');
712 aliceTestClient.httpBackend.when(
713 'PUT', '/send/',
714 ).respond(200, function(path, content) {
715 logger.log('/send:', content);
716 expect(content.session_id).toNotEqual(megolmSessionId);
717 return {
718 event_id: '$event_id',
719 };
720 });
721
722 return Promise.all([
723 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
724 aliceTestClient.httpBackend.flushAllExpected(),
725 ]);
726 });
727 });
728
729 // https://github.com/vector-im/riot-web/issues/2676
730 it("Alice should send to her other devices", function() {
731 // for this test, we make the testOlmAccount be another of Alice's devices.
732 // it ought to get included in messages Alice sends.
733
734 let p2pSession;
735 let inboundGroupSession;
736 let decrypted;
737
738 return aliceTestClient.start().then(function() {
739 // an encrypted room with just alice
740 const syncResponse = {
741 next_batch: 1,
742 rooms: {
743 join: {},
744 },
745 };
746 syncResponse.rooms.join[ROOM_ID] = {
747 state: {
748 events: [
749 testUtils.mkEvent({
750 type: 'm.room.encryption',
751 skey: '',
752 content: {
753 algorithm: 'm.megolm.v1.aes-sha2',
754 },
755 }),
756 testUtils.mkMembership({
757 mship: 'join',
758 sender: aliceTestClient.userId,
759 }),
760 ],
761 },
762 };
763 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
764
765 // the completion of the first initialsync hould make Alice
766 // invalidate the device cache for all members in e2e rooms (ie,
767 // herself), and do a key query.
768 aliceTestClient.expectKeyQuery(
769 getTestKeysQueryResponse(aliceTestClient.userId),
770 );
771
772 return aliceTestClient.httpBackend.flushAllExpected();
773 }).then(function() {
774 // start out with the device unknown - the send should be rejected.
775 return aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
776 throw new Error("sendTextMessage failed on an unknown device");
777 }, (e) => {
778 expect(e.name).toEqual("UnknownDeviceError");
779 expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
780 expect(Object.keys(e.devices[aliceTestClient.userId])).
781 toEqual(['DEVICE_ID']);
782 });
783 }).then(function() {
784 // mark the device as known, and resend.
785 aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
786 aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
787 200, function(path, content) {
788 expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
789 .toEqual("signed_curve25519");
790 return getTestKeysClaimResponse(aliceTestClient.userId);
791 });
792
793 aliceTestClient.httpBackend.when(
794 'PUT', '/sendToDevice/m.room.encrypted/',
795 ).respond(200, function(path, content) {
796 logger.log("sendToDevice: ", content);
797 const m = content.messages[aliceTestClient.userId].DEVICE_ID;
798 const ct = m.ciphertext[testSenderKey];
799 expect(ct.type).toEqual(0); // pre-key message
800
801 p2pSession = new Olm.Session();
802 p2pSession.create_inbound(testOlmAccount, ct.body);
803 const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
804
805 expect(decrypted.type).toEqual('m.room_key');
806 inboundGroupSession = new Olm.InboundGroupSession();
807 inboundGroupSession.create(decrypted.content.session_key);
808 return {};
809 });
810
811 aliceTestClient.httpBackend.when(
812 'PUT', '/send/',
813 ).respond(200, function(path, content) {
814 const ct = content.ciphertext;
815 const r = inboundGroupSession.decrypt(ct);
816 logger.log('Decrypted received megolm message', r);
817 decrypted = JSON.parse(r.plaintext);
818
819 return {
820 event_id: '$event_id',
821 };
822 });
823
824 // Grab the event that we'll need to resend
825 const room = aliceTestClient.client.getRoom(ROOM_ID);
826 const pendingEvents = room.getPendingEvents();
827 expect(pendingEvents.length).toEqual(1);
828 const unsentEvent = pendingEvents[0];
829
830 return Promise.all([
831 aliceTestClient.client.resendEvent(unsentEvent, room),
832
833 // the crypto stuff can take a while, so give the requests a whole second.
834 aliceTestClient.httpBackend.flushAllExpected({
835 timeout: 1000,
836 }),
837 ]);
838 }).then(function() {
839 expect(decrypted.type).toEqual('m.room.message');
840 expect(decrypted.content.body).toEqual('test');
841 });
842 });
843
844
845 it('Alice should wait for device list to complete when sending a megolm message',
846 function() {
847 let downloadPromise;
848 let sendPromise;
849
850 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
851 return aliceTestClient.start().then(() => {
852 // establish an olm session with alice
853 return createOlmSession(testOlmAccount, aliceTestClient);
854 }).then((p2pSession) => {
855 const syncResponse = getSyncResponse(['@bob:xyz']);
856
857 const olmEvent = encryptOlmEvent({
858 senderKey: testSenderKey,
859 recipient: aliceTestClient,
860 p2pSession: p2pSession,
861 });
862
863 syncResponse.to_device = { events: [olmEvent] };
864
865 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
866 return aliceTestClient.flushSync();
867 }).then(function() {
868 // this will block
869 logger.log('Forcing alice to download our device keys');
870 downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
871
872 // so will this.
873 sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
874 .then(() => {
875 throw new Error("sendTextMessage failed on an unknown device");
876 }, (e) => {
877 expect(e.name).toEqual("UnknownDeviceError");
878 });
879
880 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
881 200, getTestKeysQueryResponse('@bob:xyz'),
882 );
883
884 return aliceTestClient.httpBackend.flushAllExpected();
885 }).then(function() {
886 return Promise.all([downloadPromise, sendPromise]);
887 });
888 });
889
890
891 it("Alice exports megolm keys and imports them to a new device", function() {
892 let messageEncrypted;
893
894 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
895 return aliceTestClient.start().then(() => {
896 // establish an olm session with alice
897 return createOlmSession(testOlmAccount, aliceTestClient);
898 }).then((p2pSession) => {
899 const groupSession = new Olm.OutboundGroupSession();
900 groupSession.create();
901
902 // make the room_key event
903 const roomKeyEncrypted = encryptGroupSessionKey({
904 senderKey: testSenderKey,
905 recipient: aliceTestClient,
906 p2pSession: p2pSession,
907 groupSession: groupSession,
908 room_id: ROOM_ID,
909 });
910
911 // encrypt a message with the group session
912 messageEncrypted = encryptMegolmEvent({
913 senderKey: testSenderKey,
914 groupSession: groupSession,
915 room_id: ROOM_ID,
916 });
917
918 // Alice gets both the events in a single sync
919 const syncResponse = {
920 next_batch: 1,
921 to_device: {
922 events: [roomKeyEncrypted],
923 },
924 rooms: {
925 join: {},
926 },
927 };
928 syncResponse.rooms.join[ROOM_ID] = {
929 timeline: {
930 events: [messageEncrypted],
931 },
932 };
933
934 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
935 return aliceTestClient.flushSync();
936 }).then(function() {
937 const room = aliceTestClient.client.getRoom(ROOM_ID);
938 const event = room.getLiveTimeline().getEvents()[0];
939 expect(event.getContent().body).toEqual('42');
940
941 return aliceTestClient.client.exportRoomKeys();
942 }).then(function(exported) {
943 // start a new client
944 aliceTestClient.stop();
945
946 aliceTestClient = new TestClient(
947 "@alice:localhost", "device2", "access_token2",
948 );
949 return aliceTestClient.client.initCrypto().then(() => {
950 aliceTestClient.client.importRoomKeys(exported);
951 return aliceTestClient.start();
952 });
953 }).then(function() {
954 const syncResponse = {
955 next_batch: 1,
956 rooms: {
957 join: {},
958 },
959 };
960 syncResponse.rooms.join[ROOM_ID] = {
961 timeline: {
962 events: [messageEncrypted],
963 },
964 };
965
966 aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
967 return aliceTestClient.flushSync();
968 }).then(function() {
969 const room = aliceTestClient.client.getRoom(ROOM_ID);
970 const event = room.getLiveTimeline().getEvents()[0];
971 expect(event.getContent().body).toEqual('42');
972 });
973 });
974});