@towns-protocol/encryption 0.0.253 → 0.0.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,688 +0,0 @@
1
- import { SessionKeysSchema, } from '@towns-protocol/proto';
2
- import { shortenHexString, dlog, dlogError, check, bin_toHexString, } from '@towns-protocol/dlog';
3
- import { GroupEncryptionAlgorithmId, parseGroupEncryptionAlgorithmId, } from './olmLib';
4
- import { create, fromJsonString } from '@bufbuild/protobuf';
5
- export var DecryptionStatus;
6
- (function (DecryptionStatus) {
7
- DecryptionStatus["initializing"] = "initializing";
8
- DecryptionStatus["updating"] = "updating";
9
- DecryptionStatus["working"] = "working";
10
- DecryptionStatus["idle"] = "idle";
11
- DecryptionStatus["done"] = "done";
12
- })(DecryptionStatus || (DecryptionStatus = {}));
13
- class StreamTasks {
14
- encryptedContent = new Array();
15
- keySolicitations = new Array();
16
- isMissingKeys = false;
17
- keySolicitationsNeedsSort = false;
18
- sortKeySolicitations() {
19
- this.keySolicitations.sort((a, b) => a.respondAfter - b.respondAfter);
20
- this.keySolicitationsNeedsSort = false;
21
- }
22
- isEmpty() {
23
- return (this.encryptedContent.length === 0 &&
24
- this.keySolicitations.length === 0 &&
25
- !this.isMissingKeys);
26
- }
27
- }
28
- class StreamQueues {
29
- streams = new Map();
30
- getStreamIds() {
31
- return Array.from(this.streams.keys());
32
- }
33
- getQueue(streamId) {
34
- let tasks = this.streams.get(streamId);
35
- if (!tasks) {
36
- tasks = new StreamTasks();
37
- this.streams.set(streamId, tasks);
38
- }
39
- return tasks;
40
- }
41
- isEmpty() {
42
- for (const tasks of this.streams.values()) {
43
- if (!tasks.isEmpty()) {
44
- return false;
45
- }
46
- }
47
- return true;
48
- }
49
- toString() {
50
- const counts = Array.from(this.streams.entries()).reduce((acc, [_, stream]) => {
51
- acc['encryptedContent'] =
52
- (acc['encryptedContent'] ?? 0) + stream.encryptedContent.length;
53
- acc['streamsMissingKeys'] =
54
- (acc['streamsMissingKeys'] ?? 0) + (stream.isMissingKeys ? 1 : 0);
55
- acc['keySolicitations'] =
56
- (acc['keySolicitations'] ?? 0) + stream.keySolicitations.length;
57
- return acc;
58
- }, {});
59
- return Object.entries(counts)
60
- .map(([key, count]) => `${key}: ${count}`)
61
- .join(', ');
62
- }
63
- }
64
- /**
65
- *
66
- * Responsibilities:
67
- * 1. Download new to-device messages that happened while we were offline
68
- * 2. Decrypt new to-device messages
69
- * 3. Decrypt encrypted content
70
- * 4. Retry decryption failures, request keys for failed decryption
71
- * 5. Respond to key solicitations
72
- *
73
- *
74
- * Notes:
75
- * If in the future we started snapshotting the eventNum of the last message sent by every user,
76
- * we could use that to determine the order we send out keys, and the order that we reply to key solicitations.
77
- *
78
- * It should be easy to introduce a priority stream, where we decrypt messages from that stream first, before
79
- * anything else, so the messages show up quicky in the ui that the user is looking at.
80
- *
81
- * We need code to purge bad sessions (if someones sends us the wrong key, or a key that doesn't decrypt the message)
82
- */
83
- export class BaseDecryptionExtensions {
84
- _status = DecryptionStatus.initializing;
85
- mainQueues = {
86
- priorityTasks: new Array(),
87
- newGroupSession: new Array(),
88
- ownKeySolicitations: new Array(),
89
- };
90
- streamQueues = new StreamQueues();
91
- upToDateStreams = new Set();
92
- highPriorityIds = new Set();
93
- recentStreamIds = [];
94
- decryptionFailures = {}; // streamId: sessionId: EncryptedContentItem[]
95
- inProgressTick;
96
- timeoutId;
97
- delayMs = 1;
98
- started = false;
99
- numRecentStreamIds = 5;
100
- emitter;
101
- _onStopFn;
102
- log;
103
- crypto;
104
- entitlementDelegate;
105
- userDevice;
106
- userId;
107
- constructor(emitter, crypto, entitlementDelegate, userDevice, userId, upToDateStreams, inLogId) {
108
- this.emitter = emitter;
109
- this.crypto = crypto;
110
- this.entitlementDelegate = entitlementDelegate;
111
- this.userDevice = userDevice;
112
- this.userId = userId;
113
- // initialize with a set of up-to-date streams
114
- // ready for processing
115
- this.upToDateStreams = upToDateStreams;
116
- const shortKey = shortenHexString(userDevice.deviceKey);
117
- const logId = `${inLogId}:${shortKey}`;
118
- this.log = {
119
- debug: dlog('csb:decryption:debug', { defaultEnabled: false }).extend(logId),
120
- info: dlog('csb:decryption', { defaultEnabled: true }).extend(logId),
121
- error: dlogError('csb:decryption:error').extend(logId),
122
- };
123
- this.log.debug('new DecryptionExtensions', { userDevice });
124
- }
125
- enqueueNewGroupSessions(sessions, _senderId) {
126
- this.log.debug('enqueueNewGroupSessions', sessions);
127
- const streamId = bin_toHexString(sessions.streamId);
128
- this.mainQueues.newGroupSession.push({ streamId, sessions });
129
- this.checkStartTicking();
130
- }
131
- enqueueNewEncryptedContent(streamId, eventId, kind, // kind of encrypted data
132
- encryptedData) {
133
- // dms, channels, gdms ("we're in the wrong package")
134
- if (streamId.startsWith('20') || streamId.startsWith('88') || streamId.startsWith('77')) {
135
- this.recentStreamIds.push(streamId);
136
- if (this.recentStreamIds.length > this.numRecentStreamIds) {
137
- this.recentStreamIds.shift();
138
- }
139
- }
140
- this.streamQueues.getQueue(streamId).encryptedContent.push({
141
- streamId,
142
- eventId,
143
- kind,
144
- encryptedData,
145
- });
146
- this.checkStartTicking();
147
- }
148
- enqueueInitKeySolicitations(streamId, eventHashStr, members, sigBundle) {
149
- const streamQueue = this.streamQueues.getQueue(streamId);
150
- streamQueue.keySolicitations = [];
151
- this.mainQueues.ownKeySolicitations = this.mainQueues.ownKeySolicitations.filter((x) => x.streamId !== streamId);
152
- for (const member of members) {
153
- const { userId: fromUserId, userAddress: fromUserAddress } = member;
154
- for (const keySolicitation of member.solicitations) {
155
- if (keySolicitation.deviceKey === this.userDevice.deviceKey) {
156
- continue;
157
- }
158
- if (keySolicitation.sessionIds.length === 0) {
159
- continue;
160
- }
161
- const selectedQueue = fromUserId === this.userId
162
- ? this.mainQueues.ownKeySolicitations
163
- : streamQueue.keySolicitations;
164
- selectedQueue.push({
165
- streamId,
166
- fromUserId,
167
- fromUserAddress,
168
- solicitation: keySolicitation,
169
- respondAfter: Date.now() + this.getRespondDelayMSForKeySolicitation(streamId, fromUserId),
170
- sigBundle,
171
- hashStr: eventHashStr,
172
- });
173
- }
174
- }
175
- streamQueue.keySolicitationsNeedsSort = true;
176
- this.checkStartTicking();
177
- }
178
- enqueueKeySolicitation(streamId, eventHashStr, fromUserId, fromUserAddress, keySolicitation, sigBundle) {
179
- if (keySolicitation.deviceKey === this.userDevice.deviceKey) {
180
- //this.log.debug('ignoring key solicitation for our own device')
181
- return;
182
- }
183
- const streamQueue = this.streamQueues.getQueue(streamId);
184
- const selectedQueue = fromUserId === this.userId
185
- ? this.mainQueues.ownKeySolicitations
186
- : streamQueue.keySolicitations;
187
- const index = selectedQueue.findIndex((x) => x.streamId === streamId && x.solicitation.deviceKey === keySolicitation.deviceKey);
188
- if (index > -1) {
189
- selectedQueue.splice(index, 1);
190
- }
191
- if (keySolicitation.sessionIds.length > 0 || keySolicitation.isNewDevice) {
192
- //this.log.debug('new key solicitation', { fromUserId, streamId, keySolicitation })
193
- streamQueue.keySolicitationsNeedsSort = true;
194
- selectedQueue.push({
195
- streamId,
196
- fromUserId,
197
- fromUserAddress,
198
- solicitation: keySolicitation,
199
- respondAfter: Date.now() + this.getRespondDelayMSForKeySolicitation(streamId, fromUserId),
200
- sigBundle,
201
- hashStr: eventHashStr,
202
- });
203
- this.checkStartTicking();
204
- }
205
- else if (index > -1) {
206
- //this.log.debug('cleared key solicitation', keySolicitation)
207
- }
208
- }
209
- setStreamUpToDate(streamId) {
210
- //this.log.debug('streamUpToDate', streamId)
211
- this.upToDateStreams.add(streamId);
212
- this.checkStartTicking();
213
- }
214
- resetUpToDateStreams() {
215
- this.upToDateStreams.clear();
216
- this.checkStartTicking();
217
- }
218
- retryDecryptionFailures(streamId) {
219
- const streamQueue = this.streamQueues.getQueue(streamId);
220
- if (this.decryptionFailures[streamId] &&
221
- Object.keys(this.decryptionFailures[streamId]).length > 0) {
222
- this.log.debug('membership change, re-enqueuing decryption failures for stream', streamId);
223
- streamQueue.isMissingKeys = true;
224
- this.checkStartTicking();
225
- }
226
- }
227
- start() {
228
- check(!this.started, 'start() called twice, please re-instantiate instead');
229
- this.log.debug('starting');
230
- this.started = true;
231
- // let the subclass override and do any custom startup tasks
232
- this.onStart();
233
- // enqueue a task to upload device keys
234
- this.mainQueues.priorityTasks.push(() => this.uploadDeviceKeys());
235
- // enqueue a task to download new to-device messages
236
- this.enqueueNewMessageDownload();
237
- // start the tick loop
238
- this.checkStartTicking();
239
- }
240
- // enqueue a task to download new to-device messages, should be safe to call multiple times
241
- enqueueNewMessageDownload() {
242
- this.mainQueues.priorityTasks.push(() => this.downloadNewMessages());
243
- }
244
- onStart() {
245
- // let the subclass override and do any custom startup tasks
246
- }
247
- async stop() {
248
- this._onStopFn?.();
249
- this._onStopFn = undefined;
250
- // let the subclass override and do any custom shutdown tasks
251
- await this.onStop();
252
- await this.stopTicking();
253
- }
254
- onStop() {
255
- // let the subclass override and do any custom shutdown tasks
256
- return Promise.resolve();
257
- }
258
- get status() {
259
- return this._status;
260
- }
261
- setStatus(status) {
262
- if (this._status !== status) {
263
- this.log.debug(`status changed ${status}`);
264
- this._status = status;
265
- this.emitter.emit('decryptionExtStatusChanged', status);
266
- }
267
- }
268
- compareStreamIds(a, b) {
269
- const recentStreamIds = new Set(this.recentStreamIds);
270
- return (this.getPriorityForStream(a, this.highPriorityIds, recentStreamIds) -
271
- this.getPriorityForStream(b, this.highPriorityIds, recentStreamIds));
272
- }
273
- lastPrintedAt = 0;
274
- checkStartTicking() {
275
- if (!this.started ||
276
- this.timeoutId ||
277
- !this._onStopFn ||
278
- !this.isUserInboxStreamUpToDate(this.upToDateStreams) ||
279
- this.shouldPauseTicking()) {
280
- return;
281
- }
282
- if (!Object.values(this.mainQueues).find((q) => q.length > 0) &&
283
- this.streamQueues.isEmpty()) {
284
- this.setStatus(DecryptionStatus.done);
285
- return;
286
- }
287
- if (Date.now() - this.lastPrintedAt > 30000) {
288
- this.log.info(`status: ${this.status} queues: ${Object.entries(this.mainQueues)
289
- .map(([key, q]) => `${key}: ${q.length}`)
290
- .join(', ')} ${this.streamQueues.toString()}`);
291
- const streamIds = Array.from(this.streamQueues.streams.entries())
292
- .filter(([_, value]) => !value.isEmpty())
293
- .map(([key, _]) => key)
294
- .sort((a, b) => this.compareStreamIds(a, b));
295
- const first4Priority = streamIds
296
- .filter((x) => this.upToDateStreams.has(x))
297
- .slice(0, 4)
298
- .join(', ');
299
- const first4Blocked = streamIds
300
- .filter((x) => !this.upToDateStreams.has(x))
301
- .slice(0, 4)
302
- .join(', ');
303
- if (first4Priority.length > 0 || first4Blocked.length > 0) {
304
- this.log.info(`priorityTasks: ${first4Priority} waitingFor: ${first4Blocked}`);
305
- }
306
- this.lastPrintedAt = Date.now();
307
- }
308
- this.timeoutId = setTimeout(() => {
309
- this.inProgressTick = this.tick();
310
- this.inProgressTick
311
- .catch((e) => this.log.error('ProcessTick Error', e))
312
- .finally(() => {
313
- this.timeoutId = undefined;
314
- setTimeout(() => this.checkStartTicking());
315
- });
316
- }, this.getDelayMs());
317
- }
318
- async stopTicking() {
319
- if (this.timeoutId) {
320
- clearTimeout(this.timeoutId);
321
- this.timeoutId = undefined;
322
- }
323
- if (this.inProgressTick) {
324
- try {
325
- await this.inProgressTick;
326
- }
327
- catch (e) {
328
- this.log.error('ProcessTick Error while stopping', e);
329
- }
330
- finally {
331
- this.inProgressTick = undefined;
332
- }
333
- }
334
- }
335
- getDelayMs() {
336
- if (this.mainQueues.newGroupSession.length > 0) {
337
- return 0;
338
- }
339
- else {
340
- return this.delayMs;
341
- }
342
- }
343
- // just do one thing then return
344
- tick() {
345
- const now = Date.now();
346
- const priorityTask = this.mainQueues.priorityTasks.shift();
347
- if (priorityTask) {
348
- this.setStatus(DecryptionStatus.updating);
349
- return priorityTask();
350
- }
351
- // update any new group sessions
352
- const session = this.mainQueues.newGroupSession.shift();
353
- if (session) {
354
- this.setStatus(DecryptionStatus.working);
355
- return this.processNewGroupSession(session);
356
- }
357
- const ownSolicitation = this.mainQueues.ownKeySolicitations.shift();
358
- if (ownSolicitation) {
359
- this.log.debug(' processing own key solicitation');
360
- this.setStatus(DecryptionStatus.working);
361
- return this.processKeySolicitation(ownSolicitation);
362
- }
363
- const streamIds = this.streamQueues.getStreamIds();
364
- streamIds.sort((a, b) => this.compareStreamIds(a, b));
365
- for (const streamId of streamIds) {
366
- if (!this.upToDateStreams.has(streamId)) {
367
- continue;
368
- }
369
- const streamQueue = this.streamQueues.getQueue(streamId);
370
- const encryptedContent = streamQueue.encryptedContent.shift();
371
- if (encryptedContent) {
372
- this.setStatus(DecryptionStatus.working);
373
- return this.processEncryptedContentItem(encryptedContent);
374
- }
375
- if (streamQueue.isMissingKeys) {
376
- this.setStatus(DecryptionStatus.working);
377
- streamQueue.isMissingKeys = false;
378
- return this.processMissingKeys(streamId);
379
- }
380
- if (streamQueue.keySolicitationsNeedsSort) {
381
- streamQueue.sortKeySolicitations();
382
- }
383
- const keySolicitation = dequeueUpToDate(streamQueue.keySolicitations, now, (x) => x.respondAfter, this.upToDateStreams);
384
- if (keySolicitation) {
385
- this.setStatus(DecryptionStatus.working);
386
- return this.processKeySolicitation(keySolicitation);
387
- }
388
- }
389
- this.setStatus(DecryptionStatus.idle);
390
- return Promise.resolve();
391
- }
392
- /**
393
- * processNewGroupSession
394
- * process new group sessions that were sent to our to device stream inbox
395
- * re-enqueue any decryption failures with matching session id
396
- */
397
- async processNewGroupSession(sessionItem) {
398
- const { streamId, sessions: session } = sessionItem;
399
- // check if this message is to our device
400
- const ciphertext = session.ciphertexts[this.userDevice.deviceKey];
401
- if (!ciphertext) {
402
- this.log.debug('skipping, no session for our device');
403
- return;
404
- }
405
- this.log.debug('processNewGroupSession', session);
406
- // check if it contains any keys we need, default to GroupEncryption if the algorithm is not set
407
- const parsed = parseGroupEncryptionAlgorithmId(session.algorithm, GroupEncryptionAlgorithmId.GroupEncryption);
408
- if (parsed.kind === 'unrecognized') {
409
- // todo dispatch event to update the error message
410
- this.log.error('skipping, invalid algorithm', session.algorithm);
411
- return;
412
- }
413
- const algorithm = parsed.value;
414
- const neededKeyIndexs = [];
415
- for (let i = 0; i < session.sessionIds.length; i++) {
416
- const sessionId = session.sessionIds[i];
417
- const hasKeys = await this.crypto.hasSessionKey(streamId, sessionId, algorithm);
418
- if (!hasKeys) {
419
- neededKeyIndexs.push(i);
420
- }
421
- }
422
- if (!neededKeyIndexs.length) {
423
- this.log.debug('skipping, we have all the keys');
424
- return;
425
- }
426
- // decrypt the message
427
- const cleartext = await this.crypto.decryptWithDeviceKey(ciphertext, session.senderKey);
428
- const sessionKeys = fromJsonString(SessionKeysSchema, cleartext);
429
- check(sessionKeys.keys.length === session.sessionIds.length, 'bad sessionKeys');
430
- // make group sessions
431
- const sessions = neededKeyIndexs.map((i) => ({
432
- streamId: streamId,
433
- sessionId: session.sessionIds[i],
434
- sessionKey: sessionKeys.keys[i],
435
- algorithm: algorithm,
436
- }));
437
- // import the sessions
438
- this.log.debug('importing group sessions streamId:', streamId, 'count: ', sessions.length, session.sessionIds);
439
- try {
440
- await this.crypto.importSessionKeys(streamId, sessions);
441
- // re-enqueue any decryption failures with these ids
442
- const streamQueue = this.streamQueues.getQueue(streamId);
443
- for (const session of sessions) {
444
- if (this.decryptionFailures[streamId]?.[session.sessionId]) {
445
- streamQueue.encryptedContent.push(...this.decryptionFailures[streamId][session.sessionId]);
446
- delete this.decryptionFailures[streamId][session.sessionId];
447
- }
448
- }
449
- }
450
- catch (e) {
451
- // don't re-enqueue to prevent infinite loops if this session is truely corrupted
452
- // we will keep requesting it on each boot until it goes out of the scroll window
453
- this.log.error('failed to import sessions', { sessionItem, error: e });
454
- }
455
- // if we processed them all, ack the stream
456
- if (this.mainQueues.newGroupSession.length === 0) {
457
- await this.ackNewGroupSession(session);
458
- }
459
- }
460
- /**
461
- * processEncryptedContentItem
462
- * try to decrypt encrytped content
463
- */
464
- async processEncryptedContentItem(item) {
465
- this.log.debug('processEncryptedContentItem', item);
466
- try {
467
- await this.decryptGroupEvent(item.streamId, item.eventId, item.kind, item.encryptedData);
468
- }
469
- catch (err) {
470
- const sessionNotFound = isSessionNotFoundError(err);
471
- this.onDecryptionError(item, {
472
- missingSession: sessionNotFound,
473
- kind: item.kind,
474
- encryptedData: item.encryptedData,
475
- error: err,
476
- });
477
- if (sessionNotFound) {
478
- const streamId = item.streamId;
479
- const sessionId = item.encryptedData.sessionId && item.encryptedData.sessionId.length > 0
480
- ? item.encryptedData.sessionId
481
- : bin_toHexString(item.encryptedData.sessionIdBytes);
482
- if (sessionId.length === 0) {
483
- this.log.error('session id length is 0 for failed decryption', {
484
- err,
485
- streamId: item.streamId,
486
- eventId: item.eventId,
487
- });
488
- return;
489
- }
490
- if (!this.decryptionFailures[streamId]) {
491
- this.decryptionFailures[streamId] = { [sessionId]: [item] };
492
- }
493
- else if (!this.decryptionFailures[streamId][sessionId]) {
494
- this.decryptionFailures[streamId][sessionId] = [item];
495
- }
496
- else if (!this.decryptionFailures[streamId][sessionId].includes(item)) {
497
- this.decryptionFailures[streamId][sessionId].push(item);
498
- }
499
- const streamQueue = this.streamQueues.getQueue(streamId);
500
- streamQueue.isMissingKeys = true;
501
- }
502
- else {
503
- this.log.info('failed to decrypt', err, 'streamId', item.streamId);
504
- }
505
- }
506
- }
507
- /**
508
- * processMissingKeys
509
- * process missing keys and send key solicitations to streams
510
- */
511
- async processMissingKeys(streamId) {
512
- this.log.debug('processing missing keys', streamId);
513
- const missingSessionIds = takeFirst(100, Object.keys(this.decryptionFailures[streamId] ?? {}).sort());
514
- // limit to 100 keys for now todo revisit https://linear.app/hnt-labs/issue/HNT-3936/revisit-how-we-limit-the-number-of-session-ids-that-we-request
515
- if (!missingSessionIds.length) {
516
- this.log.debug('processing missing keys', streamId, 'no missing keys');
517
- return;
518
- }
519
- if (!this.hasStream(streamId)) {
520
- this.log.debug('processing missing keys', streamId, 'stream not found');
521
- return;
522
- }
523
- const isEntitled = await this.isUserEntitledToKeyExchange(streamId, this.userId, {
524
- skipOnChainValidation: true,
525
- });
526
- if (!isEntitled) {
527
- this.log.debug('processing missing keys', streamId, 'user is not member of stream');
528
- return;
529
- }
530
- const solicitedEvents = this.getKeySolicitations(streamId);
531
- const existingKeyRequest = solicitedEvents.find((x) => x.deviceKey === this.userDevice.deviceKey);
532
- if (existingKeyRequest?.isNewDevice ||
533
- sortedArraysEqual(existingKeyRequest?.sessionIds ?? [], missingSessionIds)) {
534
- this.log.debug('processing missing keys already requested keys for this session', existingKeyRequest);
535
- return;
536
- }
537
- const knownSessionIds = await this.crypto.getGroupSessionIds(streamId);
538
- const isNewDevice = knownSessionIds.length === 0;
539
- this.log.debug('requesting keys', streamId, 'isNewDevice', isNewDevice, 'sessionIds:', missingSessionIds.length);
540
- await this.sendKeySolicitation({
541
- streamId,
542
- isNewDevice,
543
- missingSessionIds,
544
- });
545
- }
546
- /**
547
- * processKeySolicitation
548
- * process incoming key solicitations and send keys and key fulfillments
549
- */
550
- async processKeySolicitation(item) {
551
- this.log.debug('processing key solicitation', item.streamId, item);
552
- const streamId = item.streamId;
553
- check(this.hasStream(streamId), 'stream not found');
554
- const { isValid, reason } = this.isValidEvent(item);
555
- if (!isValid) {
556
- this.log.error('processing key solicitation: invalid event id', {
557
- streamId,
558
- eventId: item.hashStr,
559
- reason,
560
- });
561
- return;
562
- }
563
- const knownSessionIds = await this.crypto.getGroupSessionIds(streamId);
564
- // todo split this up by algorithm so that we can send all the new hybrid keys
565
- knownSessionIds.sort();
566
- const requestedSessionIds = new Set(item.solicitation.sessionIds.sort());
567
- const replySessionIds = item.solicitation.isNewDevice
568
- ? knownSessionIds
569
- : knownSessionIds.filter((x) => requestedSessionIds.has(x));
570
- if (replySessionIds.length === 0) {
571
- this.log.debug('processing key solicitation: no keys to reply with');
572
- return;
573
- }
574
- const isUserEntitledToKeyExchange = await this.isUserEntitledToKeyExchange(streamId, item.fromUserId);
575
- if (!isUserEntitledToKeyExchange) {
576
- return;
577
- }
578
- const allSessions = [];
579
- for (const sessionId of replySessionIds) {
580
- const groupSession = await this.crypto.exportGroupSession(streamId, sessionId);
581
- if (groupSession) {
582
- allSessions.push(groupSession);
583
- }
584
- }
585
- this.log.debug('processing key solicitation with', item.streamId, {
586
- to: item.fromUserId,
587
- toDevice: item.solicitation.deviceKey,
588
- requestedCount: item.solicitation.sessionIds.length,
589
- replyIds: replySessionIds.length,
590
- sessions: allSessions.length,
591
- });
592
- if (allSessions.length === 0) {
593
- return;
594
- }
595
- // send a single key fulfillment for all algorithms
596
- const { error } = await this.sendKeyFulfillment({
597
- streamId,
598
- userAddress: item.fromUserAddress,
599
- deviceKey: item.solicitation.deviceKey,
600
- sessionIds: allSessions
601
- .map((x) => x.sessionId)
602
- .filter((x) => requestedSessionIds.has(x))
603
- .sort(),
604
- });
605
- // if the key fulfillment failed, someone else already sent a key fulfillment
606
- if (error) {
607
- if (!error.msg.includes('DUPLICATE_EVENT') && !error.msg.includes('NOT_FOUND')) {
608
- // duplicate events are expected, we can ignore them, others are not
609
- this.log.error('failed to send key fulfillment', error);
610
- }
611
- return;
612
- }
613
- // if the key fulfillment succeeded, send one group session payload for each algorithm
614
- const sessions = allSessions.reduce((acc, session) => {
615
- if (!acc[session.algorithm]) {
616
- acc[session.algorithm] = [];
617
- }
618
- acc[session.algorithm].push(session);
619
- return acc;
620
- }, {});
621
- // send one key fulfillment for each algorithm
622
- for (const kv of Object.entries(sessions)) {
623
- const algorithm = kv[0];
624
- const sessions = kv[1];
625
- await this.encryptAndShareGroupSessions({
626
- streamId,
627
- item,
628
- sessions,
629
- algorithm,
630
- });
631
- }
632
- }
633
- /**
634
- * can be overridden to add a delay to the key solicitation response
635
- */
636
- getRespondDelayMSForKeySolicitation(_streamId, _userId) {
637
- return 0;
638
- }
639
- setHighPriorityStreams(streamIds) {
640
- this.highPriorityIds = new Set(streamIds);
641
- }
642
- }
643
- export function makeSessionKeys(sessions) {
644
- const sessionKeys = sessions.map((s) => s.sessionKey);
645
- return create(SessionKeysSchema, {
646
- keys: sessionKeys,
647
- });
648
- }
649
- /// Returns the first item from the array,
650
- /// if dateFn is provided, returns the first item where dateFn(item) <= now
651
- function dequeueUpToDate(items, now, dateFn, upToDateStreams) {
652
- if (items.length === 0) {
653
- return undefined;
654
- }
655
- if (dateFn(items[0]) > now) {
656
- return undefined;
657
- }
658
- const index = items.findIndex((x) => dateFn(x) <= now && upToDateStreams.has(x.streamId));
659
- if (index === -1) {
660
- return undefined;
661
- }
662
- return items.splice(index, 1)[0];
663
- }
664
- function sortedArraysEqual(a, b) {
665
- if (a.length !== b.length) {
666
- return false;
667
- }
668
- for (let i = 0; i < a.length; i++) {
669
- if (a[i] !== b[i]) {
670
- return false;
671
- }
672
- }
673
- return true;
674
- }
675
- function takeFirst(count, array) {
676
- const result = [];
677
- for (let i = 0; i < count && i < array.length; i++) {
678
- result.push(array[i]);
679
- }
680
- return result;
681
- }
682
- function isSessionNotFoundError(err) {
683
- if (err !== null && typeof err === 'object' && 'message' in err) {
684
- return err.message.toLowerCase().includes('session not found');
685
- }
686
- return false;
687
- }
688
- //# sourceMappingURL=decryptionExtensions.js.map