@wireapp/core 17.21.1 → 17.24.0

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.
@@ -20,7 +20,12 @@
20
20
  import UUID from 'uuidjs';
21
21
  import {StatusCodes} from 'http-status-codes';
22
22
  import {APIClient} from '@wireapp/api-client';
23
- import {MessageSendingStatus, QualifiedOTRRecipients} from '@wireapp/api-client/src/conversation';
23
+ import {
24
+ MessageSendingStatus,
25
+ OTRRecipients,
26
+ QualifiedOTRRecipients,
27
+ QualifiedUserClients,
28
+ } from '@wireapp/api-client/src/conversation';
24
29
  import {CryptographyService} from '../../cryptography';
25
30
  import {MessageService} from './MessageService';
26
31
 
@@ -36,60 +41,142 @@ type TestUser = {id: string; domain: string; clients: string[]};
36
41
  const user1: TestUser = {id: UUID.genV4().toString(), domain: '1.wire.test', clients: ['client1.1', 'client1.2']};
37
42
  const user2: TestUser = {id: UUID.genV4().toString(), domain: '2.wire.test', clients: ['client2.1', 'client2.2']};
38
43
 
39
- function generatePayload(users: TestUser[]): QualifiedOTRRecipients {
40
- const payload: QualifiedOTRRecipients = {};
44
+ function generateQualifiedRecipients(users: TestUser[]): QualifiedUserClients {
45
+ const payload: QualifiedUserClients = {};
41
46
  users.forEach(({id, domain, clients}) => {
42
47
  payload[domain] ||= {};
43
- payload[domain][id] = {};
44
- clients.forEach(client => (payload[domain][id][client] = new Uint8Array()));
48
+ payload[domain][id] = clients;
45
49
  });
46
50
  return payload;
47
51
  }
48
52
 
53
+ function fakeEncrypt(_: unknown, recipients: QualifiedUserClients): Promise<QualifiedOTRRecipients> {
54
+ const encryptedPayload = Object.entries(recipients).reduce((acc, [domain, users]) => {
55
+ acc[domain] = Object.entries(users).reduce((userClients, [userId, clients]) => {
56
+ userClients[userId] = clients.reduce((payloads, client) => {
57
+ payloads[client] = new Uint8Array();
58
+ return payloads;
59
+ }, {} as any);
60
+ return userClients;
61
+ }, {} as OTRRecipients<Uint8Array>);
62
+ return acc;
63
+ }, {} as QualifiedOTRRecipients);
64
+ return Promise.resolve(encryptedPayload);
65
+ }
66
+
49
67
  describe('MessageService', () => {
50
68
  const apiClient = new APIClient();
51
69
  const cryptographyService = new CryptographyService(apiClient, {} as any);
52
70
  const messageService = new MessageService(apiClient, cryptographyService);
71
+
72
+ beforeEach(() => {
73
+ spyOn(cryptographyService, 'encryptQualified').and.callFake(fakeEncrypt);
74
+ });
75
+
53
76
  describe('sendFederatedMessage', () => {
54
77
  it('sends a message', async () => {
55
78
  spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.returnValue(Promise.resolve(baseMessageSendingStatus));
56
- const recipients: QualifiedOTRRecipients = generatePayload([user1, user2]);
79
+ const recipients = generateQualifiedRecipients([user1, user2]);
57
80
 
58
- await messageService.sendFederatedOTRMessage('senderclientid', 'convid', '', recipients, new Uint8Array());
81
+ await messageService.sendFederatedOTRMessage(
82
+ 'senderclientid',
83
+ {id: 'convid', domain: ''},
84
+ recipients,
85
+ new Uint8Array(),
86
+ );
59
87
  expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalled();
60
88
  });
61
89
 
62
- it('handles mismatch errors internally if reportMissing is true', async () => {
63
- let spyCounter = 0;
64
- spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
65
- spyCounter++;
66
- if (spyCounter === 1) {
90
+ describe('client mismatch', () => {
91
+ it('handles client mismatch internally if no onClientMismatch is given', async () => {
92
+ let spyCounter = 0;
93
+ const clientMismatch = {
94
+ ...baseMessageSendingStatus,
95
+ deleted: {[user1.domain]: {[user1.id]: [user1.clients[0]]}},
96
+ missing: {'2.wire.test': {[user2.id]: ['client22']}},
97
+ };
98
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
99
+ spyCounter++;
100
+ if (spyCounter === 1) {
101
+ const error = new Error();
102
+ (error as any).response = {
103
+ status: StatusCodes.PRECONDITION_FAILED,
104
+ data: clientMismatch,
105
+ };
106
+ return Promise.reject(error);
107
+ }
108
+ return Promise.resolve(baseMessageSendingStatus);
109
+ });
110
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
111
+
112
+ const recipients = generateQualifiedRecipients([user1, user2]);
113
+
114
+ await messageService.sendFederatedOTRMessage(
115
+ 'senderclientid',
116
+ {id: 'convid', domain: ''},
117
+ recipients,
118
+ new Uint8Array(),
119
+ {reportMissing: true},
120
+ );
121
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
122
+ });
123
+
124
+ it('continues message sending if onClientMismatch returns true', async () => {
125
+ const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(true));
126
+ const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
127
+ let spyCounter = 0;
128
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
129
+ spyCounter++;
130
+ if (spyCounter === 1) {
131
+ const error = new Error();
132
+ (error as any).response = {
133
+ status: StatusCodes.PRECONDITION_FAILED,
134
+ data: clientMismatch,
135
+ };
136
+ return Promise.reject(error);
137
+ }
138
+ return Promise.resolve(baseMessageSendingStatus);
139
+ });
140
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
141
+
142
+ const recipients = generateQualifiedRecipients([user1, user2]);
143
+
144
+ await messageService.sendFederatedOTRMessage(
145
+ 'senderclientid',
146
+ {id: 'convid', domain: ''},
147
+ recipients,
148
+ new Uint8Array(),
149
+ {reportMissing: true, onClientMismatch},
150
+ );
151
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
152
+ expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
153
+ });
154
+
155
+ it('stops message sending if onClientMismatch returns false', async () => {
156
+ const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(false));
157
+ const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
158
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
67
159
  const error = new Error();
68
160
  (error as any).response = {
69
161
  status: StatusCodes.PRECONDITION_FAILED,
70
- data: {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}},
162
+ data: clientMismatch,
71
163
  };
72
164
  return Promise.reject(error);
73
- }
74
- return Promise.resolve(baseMessageSendingStatus);
75
- });
76
- spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
77
- spyOn(cryptographyService, 'encryptQualified').and.returnValue(
78
- Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
79
- );
165
+ });
166
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
80
167
 
81
- const recipients: QualifiedOTRRecipients = generatePayload([user1, user2]);
168
+ const recipients = generateQualifiedRecipients([user1, user2]);
82
169
 
83
- await messageService.sendFederatedOTRMessage(
84
- 'senderclientid',
85
- 'convid',
86
- '',
87
- recipients,
88
- new Uint8Array(),
89
- undefined,
90
- true,
91
- );
92
- expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
170
+ await messageService.sendFederatedOTRMessage(
171
+ 'senderclientid',
172
+ {id: 'convid', domain: ''},
173
+ recipients,
174
+ new Uint8Array(),
175
+ {reportMissing: true, onClientMismatch},
176
+ );
177
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(1);
178
+ expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
179
+ });
93
180
  });
94
181
  });
95
182
  });
@@ -35,11 +35,9 @@ import {
35
35
  import {Decoder, Encoder} from 'bazinga64';
36
36
 
37
37
  import {CryptographyService} from '../../cryptography';
38
+ import {QualifiedId, QualifiedUserPreKeyBundleMap} from '@wireapp/api-client/src/user';
38
39
 
39
- type ClientMismatchError = AxiosError<{
40
- deleted: UserClients;
41
- missing: UserClients;
42
- }>;
40
+ type ClientMismatchError = AxiosError<ClientMismatch>;
43
41
 
44
42
  export class MessageService {
45
43
  constructor(private readonly apiClient: APIClient, private readonly cryptographyService: CryptographyService) {}
@@ -75,8 +73,11 @@ export class MessageService {
75
73
  ignoreMissing,
76
74
  );
77
75
  } catch (error) {
76
+ if (!this.isClientMismatchError(error)) {
77
+ throw error;
78
+ }
78
79
  const reEncryptedMessage = await this.onClientMismatch(
79
- error as AxiosError,
80
+ error.response!.data,
80
81
  {...message, data: base64CipherText ? Decoder.fromBase64(base64CipherText).asBytes : undefined, recipients},
81
82
  plainTextArray,
82
83
  );
@@ -104,14 +105,18 @@ export class MessageService {
104
105
  'redundant',
105
106
  ];
106
107
 
107
- if (messageData.ignoreOnly?.userIds?.length) {
108
- const allFailed: QualifiedUserClients = {
109
- ...messageSendingStatus.deleted,
110
- ...messageSendingStatus.failed_to_send,
111
- ...messageSendingStatus.missing,
112
- ...messageSendingStatus.redundant,
113
- };
108
+ const allFailed: QualifiedUserClients = {
109
+ ...messageSendingStatus.deleted,
110
+ ...messageSendingStatus.failed_to_send,
111
+ ...messageSendingStatus.missing,
112
+ ...messageSendingStatus.redundant,
113
+ };
114
+ const hasDiffs = Object.keys(allFailed).length;
115
+ if (!hasDiffs) {
116
+ return null;
117
+ }
114
118
 
119
+ if (messageData.ignoreOnly?.userIds?.length) {
115
120
  for (const [domainFailed, userClientsFailed] of Object.entries(allFailed)) {
116
121
  for (const userIdMissing of Object.keys(userClientsFailed)) {
117
122
  const userIsIgnored = messageData.ignoreOnly.userIds.find(({domain: domainIgnore, id: userIdIgnore}) => {
@@ -148,14 +153,18 @@ export class MessageService {
148
153
 
149
154
  public async sendFederatedOTRMessage(
150
155
  sendingClientId: string,
151
- conversationId: string,
152
- conversationDomain: string,
153
- recipients: QualifiedOTRRecipients,
156
+ {id: conversationId, domain}: QualifiedId,
157
+ recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
154
158
  plainTextArray: Uint8Array,
155
- assetData?: Uint8Array,
156
- reportMissing?: boolean,
159
+ options: {
160
+ assetData?: Uint8Array;
161
+ reportMissing?: boolean;
162
+ onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
163
+ } = {},
157
164
  ): Promise<MessageSendingStatus> {
158
- const qualifiedUserEntries = Object.entries(recipients).map<ProtobufOTR.IQualifiedUserEntry>(
165
+ const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
166
+
167
+ const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
159
168
  ([domain, otrRecipients]) => {
160
169
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
161
170
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -186,8 +195,8 @@ export class MessageService {
186
195
  },
187
196
  });
188
197
 
189
- if (assetData) {
190
- protoMessage.blob = assetData;
198
+ if (options.assetData) {
199
+ protoMessage.blob = options.assetData;
191
200
  }
192
201
 
193
202
  /*
@@ -195,31 +204,33 @@ export class MessageService {
195
204
  * missing clients. We have to ignore missing clients because there can be the case that there are clients that
196
205
  * don't provide PreKeys (clients from the Pre-E2EE era).
197
206
  */
198
- if (reportMissing) {
207
+ if (options.reportMissing) {
199
208
  protoMessage.reportAll = {};
200
209
  } else {
201
210
  protoMessage.ignoreAll = {};
202
211
  }
203
212
 
204
213
  let sendingStatus: MessageSendingStatus;
214
+ let sendingFailed: boolean = false;
205
215
  try {
206
- sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(
207
- conversationId,
208
- conversationDomain,
209
- protoMessage,
210
- );
216
+ sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, protoMessage);
211
217
  } catch (error) {
212
218
  if (!this.isClientMismatchError(error)) {
213
219
  throw error;
214
220
  }
215
221
  sendingStatus = error.response!.data! as unknown as MessageSendingStatus;
222
+ sendingFailed = true;
216
223
  }
217
224
 
218
225
  const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
219
226
 
220
227
  if (mismatch) {
221
- const reEncryptedMessage = await this.encryptForMissingClients(protoMessage, mismatch, plainTextArray);
222
- await this.apiClient.conversation.api.postOTRMessageV2(conversationId, conversationDomain, reEncryptedMessage);
228
+ const shouldStopSending = options.onClientMismatch && !(await options.onClientMismatch(mismatch));
229
+ if (shouldStopSending || !sendingFailed) {
230
+ return sendingStatus;
231
+ }
232
+ const reEncryptedMessage = await this.onFederatedMismatch(protoMessage, mismatch, plainTextArray);
233
+ await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, reEncryptedMessage);
223
234
  }
224
235
  return sendingStatus;
225
236
  }
@@ -282,7 +293,11 @@ export class MessageService {
282
293
  ignoreMissing,
283
294
  );
284
295
  } catch (error) {
285
- const reEncryptedMessage = await this.onClientProtobufMismatch(error as AxiosError, protoMessage, plainTextArray);
296
+ if (!this.isClientMismatchError(error)) {
297
+ throw error;
298
+ }
299
+ const mismatch = error.response!.data;
300
+ const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
286
301
  if (conversationId === null) {
287
302
  return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
288
303
  }
@@ -296,189 +311,188 @@ export class MessageService {
296
311
  }
297
312
 
298
313
  private async onClientMismatch(
299
- error: AxiosError,
314
+ clientMismatch: ClientMismatch,
300
315
  message: NewOTRMessage<Uint8Array>,
301
316
  plainTextArray: Uint8Array,
302
317
  ): Promise<NewOTRMessage<Uint8Array>> {
303
- if (error.response?.status === HTTP_STATUS.PRECONDITION_FAILED) {
304
- const {missing, deleted} = (error as ClientMismatchError).response?.data!;
305
-
306
- const deletedUserIds = Object.keys(deleted);
307
- const missingUserIds = Object.keys(missing);
308
-
309
- if (deletedUserIds.length) {
310
- for (const deletedUserId of deletedUserIds) {
311
- for (const deletedClientId of deleted[deletedUserId]) {
312
- const deletedUser = message.recipients[deletedUserId];
313
- if (deletedUser) {
314
- delete deletedUser[deletedClientId];
315
- }
318
+ const {missing, deleted} = clientMismatch;
319
+
320
+ const deletedUserIds = Object.keys(deleted);
321
+ const missingUserIds = Object.keys(missing);
322
+
323
+ if (deletedUserIds.length) {
324
+ for (const deletedUserId of deletedUserIds) {
325
+ for (const deletedClientId of deleted[deletedUserId]) {
326
+ const deletedUser = message.recipients[deletedUserId];
327
+ if (deletedUser) {
328
+ delete deletedUser[deletedClientId];
316
329
  }
317
330
  }
318
331
  }
332
+ }
319
333
 
320
- if (missingUserIds.length) {
321
- const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
322
- const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
323
- for (const missingUserId of missingUserIds) {
324
- for (const missingClientId in reEncryptedPayloads[missingUserId]) {
325
- const missingUser = message.recipients[missingUserId];
326
- if (!missingUser) {
327
- message.recipients[missingUserId] = {};
328
- }
329
-
330
- message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
334
+ if (missingUserIds.length) {
335
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
336
+ const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
337
+ for (const missingUserId of missingUserIds) {
338
+ for (const missingClientId in reEncryptedPayloads[missingUserId]) {
339
+ const missingUser = message.recipients[missingUserId];
340
+ if (!missingUser) {
341
+ message.recipients[missingUserId] = {};
331
342
  }
343
+
344
+ message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
332
345
  }
333
346
  }
334
-
335
- return message;
336
347
  }
337
348
 
338
- throw error;
349
+ return message;
339
350
  }
340
351
 
341
352
  private async onClientProtobufMismatch(
342
- error: AxiosError,
353
+ clientMismatch: {missing: UserClients; deleted: UserClients},
343
354
  message: ProtobufOTR.NewOtrMessage,
344
355
  plainTextArray: Uint8Array,
345
356
  ): Promise<ProtobufOTR.NewOtrMessage> {
346
- if (error.response?.status === HTTP_STATUS.PRECONDITION_FAILED) {
347
- const {missing, deleted} = (error as ClientMismatchError).response?.data!;
348
-
349
- const deletedUserIds = Object.keys(deleted);
350
- const missingUserIds = Object.keys(missing);
351
-
352
- if (deletedUserIds.length) {
353
- for (const deletedUserId of deletedUserIds) {
354
- for (const deletedClientId of deleted[deletedUserId]) {
355
- const deletedUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === deletedUserId);
356
- if (deletedUserIndex > -1) {
357
- const deletedClientIndex = message.recipients[deletedUserIndex].clients?.findIndex(({client}) => {
358
- return client.client.toString(16) === deletedClientId;
359
- });
360
- if (typeof deletedClientIndex !== 'undefined' && deletedClientIndex > -1) {
361
- delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
362
- }
357
+ const {missing, deleted} = clientMismatch;
358
+
359
+ const deletedUserIds = Object.keys(deleted);
360
+ const missingUserIds = Object.keys(missing);
361
+
362
+ if (deletedUserIds.length) {
363
+ for (const deletedUserId of deletedUserIds) {
364
+ for (const deletedClientId of deleted[deletedUserId]) {
365
+ const deletedUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === deletedUserId);
366
+ if (deletedUserIndex > -1) {
367
+ const deletedClientIndex = message.recipients[deletedUserIndex].clients?.findIndex(({client}) => {
368
+ return client.client.toString(16) === deletedClientId;
369
+ });
370
+ if (typeof deletedClientIndex !== 'undefined' && deletedClientIndex > -1) {
371
+ delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
363
372
  }
364
373
  }
365
374
  }
366
375
  }
376
+ }
367
377
 
368
- if (missingUserIds.length) {
369
- const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
370
- const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
371
- for (const missingUserId of missingUserIds) {
372
- for (const missingClientId in reEncryptedPayloads[missingUserId]) {
373
- const missingUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
374
- if (missingUserIndex === -1) {
375
- message.recipients.push({
376
- clients: [
377
- {
378
- client: {
379
- client: Long.fromString(missingClientId, 16),
380
- },
381
- text: reEncryptedPayloads[missingUserId][missingClientId],
378
+ if (missingUserIds.length) {
379
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
380
+ const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
381
+ for (const missingUserId of missingUserIds) {
382
+ for (const missingClientId in reEncryptedPayloads[missingUserId]) {
383
+ const missingUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
384
+ if (missingUserIndex === -1) {
385
+ message.recipients.push({
386
+ clients: [
387
+ {
388
+ client: {
389
+ client: Long.fromString(missingClientId, 16),
382
390
  },
383
- ],
384
- user: {
385
- uuid: uuidToBytes(missingUserId),
391
+ text: reEncryptedPayloads[missingUserId][missingClientId],
386
392
  },
387
- });
388
- }
393
+ ],
394
+ user: {
395
+ uuid: uuidToBytes(missingUserId),
396
+ },
397
+ });
389
398
  }
390
399
  }
391
400
  }
392
-
393
- return message;
394
401
  }
395
402
 
396
- throw error;
403
+ return message;
397
404
  }
398
405
 
399
- /**
400
- * Will re-encrypt a message when there were some missing clients in the initial send (typically when the server replies with a client mismatch error)
401
- *
402
- * @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
403
- * @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
404
- * @param {Uint8Array} plainTextArray The text that should be encrypted for the missing clients
405
- * @return resolves with a new message payload that can be sent
406
- */
407
- private async encryptForMissingClients(
408
- messageData: ProtobufOTR.QualifiedNewOtrMessage,
409
- messageSendingStatus: MessageSendingStatus,
410
- plainTextArray: Uint8Array,
411
- ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
406
+ private deleteExtraQualifiedClients(
407
+ message: ProtobufOTR.QualifiedNewOtrMessage,
408
+ deletedClients: MessageSendingStatus['deleted'],
409
+ ): ProtobufOTR.QualifiedNewOtrMessage {
412
410
  // walk through deleted domain/user map
413
- for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(messageSendingStatus.deleted)) {
414
- if (!messageData.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
411
+ for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
412
+ if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
415
413
  // no user from this domain was deleted
416
414
  continue;
417
415
  }
418
416
  // walk through deleted user ids
419
417
  for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
420
418
  // walk through message recipients
421
- for (const recipientIndex in messageData.recipients) {
419
+ for (const recipients of message.recipients) {
422
420
  // check if message recipients' domain is the same as the deleted user's domain
423
- if (messageData.recipients[recipientIndex].domain === deletedUserDomain) {
421
+ if (recipients.domain === deletedUserDomain) {
424
422
  // check if message recipients' id is the same as the deleted user's id
425
- for (const entriesIndex in messageData.recipients[recipientIndex].entries || []) {
426
- const uuid = messageData.recipients[recipientIndex].entries![entriesIndex].user?.uuid;
423
+ for (const recipientEntry of recipients.entries || []) {
424
+ const uuid = recipientEntry.user?.uuid;
427
425
  if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
428
426
  // delete this user from the message recipients
429
- delete messageData.recipients[recipientIndex].entries![entriesIndex];
427
+ const deleteIndex = recipients.entries!.indexOf(recipientEntry);
428
+ recipients.entries!.splice(deleteIndex);
430
429
  }
431
430
  }
432
431
  }
433
432
  }
434
433
  }
435
434
  }
435
+ return message;
436
+ }
436
437
 
437
- const missingUserIds = Object.entries(messageSendingStatus.missing);
438
- if (missingUserIds.length) {
439
- const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(
440
- messageSendingStatus.missing,
441
- );
442
- const reEncryptedPayloads = await this.cryptographyService.encryptQualified(plainTextArray, missingPreKeyBundles);
438
+ /**
439
+ * Will re-encrypt a message when there were some missing clients in the initial send (typically when the server replies with a client mismatch error)
440
+ *
441
+ * @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
442
+ * @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
443
+ * @param {Uint8Array} plainText The text that should be encrypted for the missing clients
444
+ * @return resolves with a new message payload that can be sent
445
+ */
446
+ private async onFederatedMismatch(
447
+ message: ProtobufOTR.QualifiedNewOtrMessage,
448
+ {deleted, missing}: MessageSendingStatus,
449
+ plainText: Uint8Array,
450
+ ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
451
+ message = this.deleteExtraQualifiedClients(message, deleted);
452
+ if (Object.keys(missing).length) {
453
+ const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(missing);
454
+ const reEncryptedPayloads = await this.cryptographyService.encryptQualified(plainText, missingPreKeyBundles);
455
+ message = this.addMissingQualifiedClients(message, reEncryptedPayloads);
456
+ }
457
+ return message;
458
+ }
443
459
 
444
- // walk through missing domain/user map
445
- for (const [missingUserDomain, missingUserIdClients] of missingUserIds) {
446
- if (!messageData.recipients.find(recipient => recipient.domain === missingUserDomain)) {
447
- // no user from this domain is missing
448
- continue;
449
- }
460
+ private addMissingQualifiedClients(
461
+ messageData: ProtobufOTR.QualifiedNewOtrMessage,
462
+ reEncryptedPayloads: QualifiedOTRRecipients,
463
+ ): ProtobufOTR.QualifiedNewOtrMessage {
464
+ // walk through missing domain/user map
465
+ for (const [missingDomain, userClients] of Object.entries(reEncryptedPayloads)) {
466
+ // walk through missing user ids
467
+ for (const [missingUserId, missingClientIds] of Object.entries(userClients)) {
468
+ // walk through message recipients
469
+ for (const domain of messageData.recipients) {
470
+ // check if message recipients' domain is the same as the missing user's domain
471
+ if (domain.domain === missingDomain) {
472
+ // check if there is a recipient with same user id as the missing user's id
473
+ let userIndex = domain.entries?.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
474
+
475
+ if (userIndex === -1) {
476
+ // no recipient found, let's create it
477
+ userIndex =
478
+ domain.entries!.push({
479
+ user: {
480
+ uuid: uuidToBytes(missingUserId),
481
+ },
482
+ }) - 1;
483
+ }
450
484
 
451
- // walk through missing user ids
452
- for (const [missingUserId, missingClientIds] of Object.entries(missingUserIdClients)) {
453
- // walk through message recipients
454
- for (const domain of messageData.recipients) {
455
- // check if message recipients' domain is the same as the missing user's domain
456
- if (domain.domain === missingUserDomain) {
457
- // check if there is a recipient with same user id as the missing user's id
458
- let userIndex = domain.entries?.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
459
-
460
- if (userIndex === -1) {
461
- // no recipient found, let's create it
462
- userIndex =
463
- domain.entries!.push({
464
- user: {
465
- uuid: uuidToBytes(missingUserId),
466
- },
467
- }) - 1;
468
- }
485
+ const missingUserUUID = domain.entries![userIndex!].user.uuid;
469
486
 
470
- const missingUserUUID = domain.entries![userIndex!].user.uuid;
471
-
472
- if (bytesToUUID(missingUserUUID) === missingUserId) {
473
- for (const missingClientId of missingClientIds) {
474
- domain.entries![userIndex!].clients ||= [];
475
- domain.entries![userIndex!].clients?.push({
476
- client: {
477
- client: Long.fromString(missingClientId, 16),
478
- },
479
- text: reEncryptedPayloads[missingUserDomain][missingUserId][missingClientId],
480
- });
481
- }
487
+ if (bytesToUUID(missingUserUUID) === missingUserId) {
488
+ for (const [missingClientId, missingClientPayload] of Object.entries(missingClientIds)) {
489
+ domain.entries![userIndex!].clients ||= [];
490
+ domain.entries![userIndex!].clients?.push({
491
+ client: {
492
+ client: Long.fromString(missingClientId, 16),
493
+ },
494
+ text: missingClientPayload,
495
+ });
482
496
  }
483
497
  }
484
498
  }