1 | import 'source-map-support/register';
|
2 |
|
3 | import '../olm-loader';
|
4 |
|
5 | import Crypto from '../../lib/crypto';
|
6 | import expect from 'expect';
|
7 |
|
8 | import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
9 | import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
10 | import MockStorageApi from '../MockStorageApi';
|
11 | import TestClient from '../TestClient';
|
12 | import {MatrixEvent} from '../../lib/models/event';
|
13 | import Room from '../../lib/models/room';
|
14 | import olmlib from '../../lib/crypto/olmlib';
|
15 | import lolex from 'lolex';
|
16 |
|
17 | const EventEmitter = require("events").EventEmitter;
|
18 |
|
19 | const sdk = require("../..");
|
20 |
|
21 | const Olm = global.Olm;
|
22 |
|
23 | describe("Crypto", function() {
|
24 | if (!sdk.CRYPTO_ENABLED) {
|
25 | return;
|
26 | }
|
27 |
|
28 | beforeEach(function(done) {
|
29 | Olm.init().then(done);
|
30 | });
|
31 |
|
32 | it("Crypto exposes the correct olm library version", function() {
|
33 | expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
34 | });
|
35 |
|
36 |
|
37 | describe('Session management', function() {
|
38 | const otkResponse = {
|
39 | one_time_keys: {
|
40 | '@alice:home.server': {
|
41 | aliceDevice: {
|
42 | 'signed_curve25519:FLIBBLE': {
|
43 | key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
44 | signatures: {
|
45 | '@alice:home.server': {
|
46 | 'ed25519:aliceDevice': 'totally a valid signature',
|
47 | },
|
48 | },
|
49 | },
|
50 | },
|
51 | },
|
52 | },
|
53 | };
|
54 | let crypto;
|
55 | let mockBaseApis;
|
56 | let mockRoomList;
|
57 |
|
58 | let fakeEmitter;
|
59 |
|
60 | beforeEach(async function() {
|
61 | const mockStorage = new MockStorageApi();
|
62 | const sessionStore = new WebStorageSessionStore(mockStorage);
|
63 | const cryptoStore = new MemoryCryptoStore(mockStorage);
|
64 |
|
65 | cryptoStore.storeEndToEndDeviceData({
|
66 | devices: {
|
67 | '@bob:home.server': {
|
68 | 'BOBDEVICE': {
|
69 | keys: {
|
70 | 'curve25519:BOBDEVICE': 'this is a key',
|
71 | },
|
72 | },
|
73 | },
|
74 | },
|
75 | trackingStatus: {},
|
76 | });
|
77 |
|
78 | mockBaseApis = {
|
79 | sendToDevice: expect.createSpy(),
|
80 | getKeyBackupVersion: expect.createSpy(),
|
81 | isGuest: expect.createSpy(),
|
82 | };
|
83 | mockRoomList = {};
|
84 |
|
85 | fakeEmitter = new EventEmitter();
|
86 |
|
87 | crypto = new Crypto(
|
88 | mockBaseApis,
|
89 | sessionStore,
|
90 | "@alice:home.server",
|
91 | "FLIBBLE",
|
92 | sessionStore,
|
93 | cryptoStore,
|
94 | mockRoomList,
|
95 | );
|
96 | crypto.registerEventHandlers(fakeEmitter);
|
97 | await crypto.init();
|
98 | });
|
99 |
|
100 | afterEach(async function() {
|
101 | await crypto.stop();
|
102 | });
|
103 |
|
104 | it("restarts wedged Olm sessions", async function() {
|
105 | const prom = new Promise((resolve) => {
|
106 | mockBaseApis.claimOneTimeKeys = function() {
|
107 | resolve();
|
108 | return otkResponse;
|
109 | };
|
110 | });
|
111 |
|
112 | fakeEmitter.emit('toDeviceEvent', {
|
113 | getType: expect.createSpy().andReturn('m.room.message'),
|
114 | getContent: expect.createSpy().andReturn({
|
115 | msgtype: 'm.bad.encrypted',
|
116 | }),
|
117 | getWireContent: expect.createSpy().andReturn({
|
118 | algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
119 | sender_key: 'this is a key',
|
120 | }),
|
121 | getSender: expect.createSpy().andReturn('@bob:home.server'),
|
122 | });
|
123 |
|
124 | await prom;
|
125 | });
|
126 | });
|
127 |
|
128 | describe('Key requests', function() {
|
129 | let aliceClient;
|
130 | let bobClient;
|
131 |
|
132 | beforeEach(async function() {
|
133 | aliceClient = (new TestClient(
|
134 | "@alice:example.com", "alicedevice",
|
135 | )).client;
|
136 | bobClient = (new TestClient(
|
137 | "@bob:example.com", "bobdevice",
|
138 | )).client;
|
139 | await aliceClient.initCrypto();
|
140 | await bobClient.initCrypto();
|
141 | });
|
142 |
|
143 | afterEach(async function() {
|
144 | aliceClient.stopClient();
|
145 | bobClient.stopClient();
|
146 | });
|
147 |
|
148 | it(
|
149 | "does not cancel keyshare requests if some messages are not decrypted",
|
150 | async function() {
|
151 | function awaitEvent(emitter, event) {
|
152 | return new Promise((resolve, reject) => {
|
153 | emitter.once(event, (result) => {
|
154 | resolve(result);
|
155 | });
|
156 | });
|
157 | }
|
158 |
|
159 | async function keyshareEventForEvent(event, index) {
|
160 | const eventContent = event.getWireContent();
|
161 | const key = await aliceClient._crypto._olmDevice
|
162 | .getInboundGroupSessionKey(
|
163 | roomId, eventContent.sender_key, eventContent.session_id,
|
164 | index,
|
165 | );
|
166 | const ksEvent = new MatrixEvent({
|
167 | type: "m.forwarded_room_key",
|
168 | sender: "@alice:example.com",
|
169 | content: {
|
170 | algorithm: olmlib.MEGOLM_ALGORITHM,
|
171 | room_id: roomId,
|
172 | sender_key: eventContent.sender_key,
|
173 | sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
174 | session_id: eventContent.session_id,
|
175 | session_key: key.key,
|
176 | chain_index: key.chain_index,
|
177 | forwarding_curve25519_key_chain:
|
178 | key.forwarding_curve_key_chain,
|
179 | },
|
180 | });
|
181 |
|
182 | ksEvent._senderCurve25519Key = "akey";
|
183 | return ksEvent;
|
184 | }
|
185 |
|
186 | const encryptionCfg = {
|
187 | "algorithm": "m.megolm.v1.aes-sha2",
|
188 | };
|
189 | const roomId = "!someroom";
|
190 | const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
191 | const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
192 | aliceClient.store.storeRoom(aliceRoom);
|
193 | bobClient.store.storeRoom(bobRoom);
|
194 | await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
195 | await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
196 | const events = [
|
197 | new MatrixEvent({
|
198 | type: "m.room.message",
|
199 | sender: "@alice:example.com",
|
200 | room_id: roomId,
|
201 | event_id: "$1",
|
202 | content: {
|
203 | msgtype: "m.text",
|
204 | body: "1",
|
205 | },
|
206 | }),
|
207 | new MatrixEvent({
|
208 | type: "m.room.message",
|
209 | sender: "@alice:example.com",
|
210 | room_id: roomId,
|
211 | event_id: "$2",
|
212 | content: {
|
213 | msgtype: "m.text",
|
214 | body: "2",
|
215 | },
|
216 | }),
|
217 | ];
|
218 | await Promise.all(events.map(async (event) => {
|
219 |
|
220 |
|
221 | await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
222 | event._clearEvent = {};
|
223 | event._senderCurve25519Key = null;
|
224 | event._claimedEd25519Key = null;
|
225 | try {
|
226 | await bobClient._crypto.decryptEvent(event);
|
227 | } catch (e) {
|
228 |
|
229 |
|
230 | }
|
231 | }));
|
232 |
|
233 | const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
234 | roomId, olmlib.MEGOLM_ALGORITHM,
|
235 | );
|
236 |
|
237 | let eventPromise = Promise.all(events.map((ev) => {
|
238 | return awaitEvent(ev, "Event.decrypted");
|
239 | }));
|
240 |
|
241 |
|
242 |
|
243 |
|
244 | let ksEvent = await keyshareEventForEvent(events[1], 1);
|
245 | await bobDecryptor.onRoomKeyEvent(ksEvent);
|
246 | await eventPromise;
|
247 | expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
248 | expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
|
249 |
|
250 | const cryptoStore = bobClient._cryptoStore;
|
251 | const eventContent = events[0].getWireContent();
|
252 | const senderKey = eventContent.sender_key;
|
253 | const sessionId = eventContent.session_id;
|
254 | const roomKeyRequestBody = {
|
255 | algorithm: olmlib.MEGOLM_ALGORITHM,
|
256 | room_id: roomId,
|
257 | sender_key: senderKey,
|
258 | session_id: sessionId,
|
259 | };
|
260 |
|
261 |
|
262 | expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
263 | .toExist();
|
264 |
|
265 |
|
266 |
|
267 | eventPromise = awaitEvent(events[0], "Event.decrypted");
|
268 | ksEvent = await keyshareEventForEvent(events[0], 0);
|
269 | await bobDecryptor.onRoomKeyEvent(ksEvent);
|
270 | await eventPromise;
|
271 | expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
|
272 |
|
273 | expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
274 | .toNotExist();
|
275 | },
|
276 | );
|
277 |
|
278 | it("creates a new keyshare request if we request a keyshare", async function() {
|
279 |
|
280 |
|
281 | const event = new MatrixEvent({
|
282 | sender: "@bob:example.com",
|
283 | room_id: "!someroom",
|
284 | content: {
|
285 | algorithm: olmlib.MEGOLM_ALGORITHM,
|
286 | session_id: "sessionid",
|
287 | sender_key: "senderkey",
|
288 | },
|
289 | });
|
290 | await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
291 | const cryptoStore = aliceClient._cryptoStore;
|
292 | const roomKeyRequestBody = {
|
293 | algorithm: olmlib.MEGOLM_ALGORITHM,
|
294 | room_id: "!someroom",
|
295 | session_id: "sessionid",
|
296 | sender_key: "senderkey",
|
297 | };
|
298 | expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
299 | .toExist();
|
300 | });
|
301 |
|
302 | it("uses a new txnid for re-requesting keys", async function() {
|
303 | const event = new MatrixEvent({
|
304 | sender: "@bob:example.com",
|
305 | room_id: "!someroom",
|
306 | content: {
|
307 | algorithm: olmlib.MEGOLM_ALGORITHM,
|
308 | session_id: "sessionid",
|
309 | sender_key: "senderkey",
|
310 | },
|
311 | });
|
312 | |
313 |
|
314 |
|
315 | function awaitFunctionCall() {
|
316 | let func;
|
317 | const promise = new Promise((resolve, reject) => {
|
318 | func = function(...args) {
|
319 | resolve(args);
|
320 | return new Promise((resolve, reject) => {
|
321 |
|
322 |
|
323 | global.setTimeout(resolve, 1);
|
324 | });
|
325 | };
|
326 | });
|
327 | return {func, promise};
|
328 | }
|
329 |
|
330 | aliceClient.startClient();
|
331 |
|
332 | const clock = lolex.install();
|
333 |
|
334 | try {
|
335 | let promise;
|
336 |
|
337 |
|
338 | ({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
339 | await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
340 | clock.runToLast();
|
341 | let args = await promise;
|
342 | const txnId = args[2];
|
343 | clock.runToLast();
|
344 |
|
345 |
|
346 |
|
347 | await Promise.resolve();
|
348 |
|
349 |
|
350 | ({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
351 | await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
352 | clock.runToLast();
|
353 |
|
354 | args = await promise;
|
355 |
|
356 | ({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
357 | clock.runToLast();
|
358 | args = await promise;
|
359 | clock.runToLast();
|
360 | expect(args[2]).toNotBe(txnId);
|
361 | } finally {
|
362 | clock.uninstall();
|
363 | }
|
364 | });
|
365 | });
|
366 | });
|