@@ -6,9 +6,14 @@ import * as anotherJson from "another-json";
6
6
import {
7
7
DeviceKeyAlgorithm ,
8
8
EncryptionAlgorithm ,
9
+ IOlmEncrypted ,
10
+ IOlmPayload ,
11
+ IOlmSession ,
9
12
OTKAlgorithm ,
10
- OTKCounts , OTKs ,
13
+ OTKCounts ,
14
+ OTKs ,
11
15
Signatures ,
16
+ UserDevice ,
12
17
} from "../models/Crypto" ;
13
18
import { requiresReady } from "./decorators" ;
14
19
import { RoomTracker } from "./RoomTracker" ;
@@ -105,6 +110,11 @@ export class CryptoClient {
105
110
this . pickledAccount = pickled ;
106
111
107
112
this . maxOTKs = account . max_number_of_one_time_keys ( ) ;
113
+
114
+ const keys = JSON . parse ( account . identity_keys ( ) ) ;
115
+ this . deviceCurve25519 = keys [ 'curve25519' ] ;
116
+ this . deviceEd25519 = keys [ 'ed25519' ] ;
117
+
108
118
this . ready = true ;
109
119
110
120
const counts = await this . client . uploadDeviceKeys ( [
@@ -120,13 +130,14 @@ export class CryptoClient {
120
130
this . pickleKey = pickleKey ;
121
131
this . pickledAccount = pickled ;
122
132
this . maxOTKs = account . max_number_of_one_time_keys ( ) ;
133
+
134
+ const keys = JSON . parse ( account . identity_keys ( ) ) ;
135
+ this . deviceCurve25519 = keys [ 'curve25519' ] ;
136
+ this . deviceEd25519 = keys [ 'ed25519' ] ;
137
+
123
138
this . ready = true ;
124
139
await this . updateCounts ( await this . client . checkOneTimeKeyCounts ( ) ) ;
125
140
}
126
-
127
- const keys = JSON . parse ( account . identity_keys ( ) ) ;
128
- this . deviceCurve25519 = keys [ 'curve25519' ] ;
129
- this . deviceEd25519 = keys [ 'ed25519' ] ;
130
141
} finally {
131
142
account . free ( ) ;
132
143
}
@@ -211,7 +222,7 @@ export class CryptoClient {
211
222
const util = new Olm . Utility ( ) ;
212
223
try {
213
224
const message = anotherJson . stringify ( obj ) ;
214
- util . ed25519_verify ( message , key , signature ) ;
225
+ util . ed25519_verify ( key , message , signature ) ;
215
226
} catch ( e ) {
216
227
// Assume it's a verification failure
217
228
return false ;
@@ -232,6 +243,142 @@ export class CryptoClient {
232
243
this . deviceTracker . flagUsersOutdated ( userIds , resync ) ;
233
244
}
234
245
246
+ /**
247
+ * Gets or creates Olm sessions for the given users and devices. Where sessions cannot be created,
248
+ * the user/device will be excluded from the returned map.
249
+ * @param {Record<string, string[]> } userDeviceMap Map of user IDs to device IDs
250
+ * @returns {Promise<Record<string, Record<string, IOlmSession>>> } Resolves to a map of user ID to device
251
+ * ID to session. Users/devices which cannot have sessions made will not be included, thus the object
252
+ * may be empty.
253
+ */
254
+ public async getOrCreateOlmSessions ( userDeviceMap : Record < string , string [ ] > ) : Promise < Record < string , Record < string , IOlmSession > > > {
255
+ const otkClaimRequest : Record < string , Record < string , OTKAlgorithm > > = { } ;
256
+ const userDeviceSessionIds : Record < string , Record < string , IOlmSession > > = { } ;
257
+
258
+ const myUserId = await this . client . getUserId ( ) ;
259
+ const myDeviceId = this . clientDeviceId ;
260
+ for ( const userId of Object . keys ( userDeviceMap ) ) {
261
+ for ( const deviceId of userDeviceMap [ userId ] ) {
262
+ if ( userId === myUserId && deviceId === myDeviceId ) {
263
+ // Skip creating a session for our own device
264
+ continue ;
265
+ }
266
+
267
+ const existingSession = await this . client . cryptoStore . getCurrentOlmSession ( userId , deviceId ) ;
268
+ if ( existingSession ) {
269
+ if ( ! userDeviceSessionIds [ userId ] ) userDeviceSessionIds [ userId ] = { } ;
270
+ userDeviceSessionIds [ userId ] [ deviceId ] = existingSession ;
271
+ } else {
272
+ if ( ! otkClaimRequest [ userId ] ) otkClaimRequest [ userId ] = { } ;
273
+ otkClaimRequest [ userId ] [ deviceId ] = OTKAlgorithm . Signed ;
274
+ }
275
+ }
276
+ }
277
+
278
+ const claimed = await this . client . claimOneTimeKeys ( otkClaimRequest ) ;
279
+ for ( const userId of Object . keys ( claimed . one_time_keys ) ) {
280
+ const storedDevices = await this . client . cryptoStore . getUserDevices ( userId ) ;
281
+ for ( const deviceId of Object . keys ( claimed . one_time_keys [ userId ] ) ) {
282
+ try {
283
+ const device = storedDevices . find ( d => d . user_id === userId && d . device_id === deviceId ) ;
284
+ if ( ! device ) {
285
+ LogService . warn ( "CryptoClient" , `Failed to handle claimed OTK: unable to locate stored device for user: ${ userId } ${ deviceId } ` ) ;
286
+ continue ;
287
+ }
288
+
289
+ const deviceKeyLabel = `${ DeviceKeyAlgorithm . Ed25119 } :${ deviceId } ` ;
290
+
291
+ const keyId = Object . keys ( claimed . one_time_keys [ userId ] [ deviceId ] ) [ 0 ] ;
292
+ const signedKey = claimed . one_time_keys [ userId ] [ deviceId ] [ keyId ] ;
293
+ const signature = signedKey ?. signatures ?. [ userId ] ?. [ deviceKeyLabel ] ;
294
+ if ( ! signature ) {
295
+ LogService . warn ( "CryptoClient" , `Failed to find appropriate signature for claimed OTK ${ userId } ${ deviceId } ` ) ;
296
+ continue ;
297
+ }
298
+
299
+ const verified = await this . verifySignature ( signedKey , device . keys [ deviceKeyLabel ] , signature ) ;
300
+ if ( ! verified ) {
301
+ LogService . warn ( "CryptoClient" , `Invalid signature for claimed OTK ${ userId } ${ deviceId } ` ) ;
302
+ continue ;
303
+ }
304
+
305
+ // TODO: Handle spec rate limiting
306
+ // Clients should rate-limit the number of sessions it creates per device that it receives a message
307
+ // from. Clients should not create a new session with another device if it has already created one
308
+ // for that given device in the past 1 hour.
309
+
310
+ // Finally, we can create a session. We do this on each loop just in case something goes wrong given
311
+ // we don't have app-level transaction support here. We want to persist as many outbound sessions as
312
+ // we can before exploding.
313
+ const account = await this . getOlmAccount ( ) ;
314
+ const session = new Olm . Session ( ) ;
315
+ try {
316
+ const curveDeviceKey = device . keys [ `${ DeviceKeyAlgorithm . Curve25519 } :${ deviceId } ` ] ;
317
+ session . create_outbound ( account , curveDeviceKey , signedKey . key ) ;
318
+ const storedSession : IOlmSession = {
319
+ sessionId : session . session_id ( ) ,
320
+ lastDecryptionTs : Date . now ( ) ,
321
+ pickled : session . pickle ( this . pickleKey ) ,
322
+ } ;
323
+ await this . client . cryptoStore . storeOlmSession ( userId , deviceId , storedSession ) ;
324
+
325
+ if ( ! userDeviceSessionIds [ userId ] ) userDeviceSessionIds [ userId ] = { } ;
326
+ userDeviceSessionIds [ userId ] [ deviceId ] = storedSession ;
327
+
328
+ // Send a dummy event so the device can prepare the session.
329
+ // await this.encryptAndSendOlmMessage(device, storedSession, "m.dummy", {});
330
+ } finally {
331
+ session . free ( ) ;
332
+ await this . storeAndFreeOlmAccount ( account ) ;
333
+ }
334
+ } catch ( e ) {
335
+ LogService . warn ( "CryptoClient" , `Unable to verify signature of claimed OTK ${ userId } ${ deviceId } :` , e ) ;
336
+ }
337
+ }
338
+ }
339
+
340
+ return userDeviceSessionIds ;
341
+ }
342
+
343
+ private async encryptAndSendOlmMessage ( device : UserDevice , session : IOlmSession , type : string , content : any ) : Promise < void > {
344
+ const olmSession = new Olm . Session ( ) ;
345
+ try {
346
+ olmSession . unpickle ( this . pickleKey , session . pickled ) ;
347
+ const payload : IOlmPayload = {
348
+ keys : {
349
+ ed25519 : this . deviceEd25519 ,
350
+ } ,
351
+ recipient_keys : {
352
+ ed25519 : device . keys [ `${ DeviceKeyAlgorithm . Ed25119 } :${ device . device_id } ` ] ,
353
+ } ,
354
+ recipient : device . user_id ,
355
+ sender : await this . client . getUserId ( ) ,
356
+ content : content ,
357
+ type : type ,
358
+ } ;
359
+ const encrypted = olmSession . encrypt ( JSON . stringify ( payload ) ) ;
360
+ await this . client . cryptoStore . storeOlmSession ( device . user_id , device . device_id , {
361
+ pickled : olmSession . pickle ( this . pickleKey ) ,
362
+ lastDecryptionTs : session . lastDecryptionTs ,
363
+ sessionId : olmSession . session_id ( ) ,
364
+ } ) ;
365
+ const message : IOlmEncrypted = {
366
+ algorithm : EncryptionAlgorithm . OlmV1Curve25519AesSha2 ,
367
+ ciphertext : {
368
+ [ device . keys [ `${ DeviceKeyAlgorithm . Curve25519 } :${ device . device_id } ` ] ] : encrypted as any ,
369
+ } ,
370
+ sender_key : this . deviceCurve25519 ,
371
+ } ;
372
+ await this . client . sendToDevices ( "m.room.encrypted" , {
373
+ [ device . user_id ] : {
374
+ [ device . device_id ] : message ,
375
+ } ,
376
+ } ) ;
377
+ } finally {
378
+ olmSession . free ( ) ;
379
+ }
380
+ }
381
+
235
382
/**
236
383
* Encrypts the details of a room event, returning an encrypted payload to be sent in an
237
384
* `m.room.encrypted` event to the room. If needed, this function will send decryption keys
@@ -243,7 +390,7 @@ export class CryptoClient {
243
390
* @param {any } content The event content being encrypted.
244
391
* @returns {Promise<any> } Resolves to the encrypted content for an `m.room.encrypted` event.
245
392
*/
246
- public async encryptEvent ( roomId : string , eventType : string , content : any ) : Promise < any > {
393
+ public async encryptRoomEvent ( roomId : string , eventType : string , content : any ) : Promise < any > {
247
394
if ( ! ( await this . isRoomEncrypted ( roomId ) ) ) {
248
395
throw new Error ( "Room is not encrypted" ) ;
249
396
}
@@ -290,14 +437,45 @@ export class CryptoClient {
290
437
try {
291
438
session . unpickle ( this . pickleKey , currentSession . pickled ) ;
292
439
440
+ const neededSessions : Record < string , string [ ] > = { } ;
293
441
for ( const userId of Object . keys ( devices ) ) {
294
- for ( const deviceId of Object . keys ( devices [ userId ] ) ) {
295
- const device = devices [ userId ] [ deviceId ] ;
296
- // TODO: Olm session management
442
+ neededSessions [ userId ] = devices [ userId ] . map ( d => d . device_id ) ;
443
+ }
444
+ const olmSessions = await this . getOrCreateOlmSessions ( neededSessions ) ;
445
+
446
+ for ( const userId of Object . keys ( devices ) ) {
447
+ for ( const device of devices [ userId ] ) {
448
+ const olmSession = olmSessions [ userId ] ?. [ device . device_id ] ;
449
+ if ( ! olmSession ) {
450
+ LogService . warn ( "CryptoClient" , `Unable to send Megolm session to ${ userId } ${ device . device_id } : No Olm session` ) ;
451
+ continue ;
452
+ }
453
+ await this . encryptAndSendOlmMessage ( device , olmSession , "m.room_key" , {
454
+ algorithm : EncryptionAlgorithm . MegolmV1AesSha2 ,
455
+ room_id : roomId ,
456
+ session_id : session . session_id ( ) ,
457
+ session_key : session . session_key ( ) ,
458
+ } ) ;
297
459
}
298
460
}
461
+
462
+ const encrypted = session . encrypt ( JSON . stringify ( {
463
+ type : eventType ,
464
+ content : content ,
465
+ room_id : roomId ,
466
+ } ) ) ;
467
+
468
+ return {
469
+ algorithm : EncryptionAlgorithm . MegolmV1AesSha2 ,
470
+ sender_key : this . deviceCurve25519 ,
471
+ ciphertext : encrypted ,
472
+ session_id : session . session_id ( ) ,
473
+ device_id : this . clientDeviceId ,
474
+ } ;
299
475
} finally {
300
476
session . free ( ) ;
301
477
}
478
+
479
+
302
480
}
303
481
}
0 commit comments