@wireapp/core 17.21.1 → 17.22.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,7 @@
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 {MessageSendingStatus, QualifiedUserClients} from '@wireapp/api-client/src/conversation';
24
24
  import {CryptographyService} from '../../cryptography';
25
25
  import {MessageService} from './MessageService';
26
26
 
@@ -36,12 +36,11 @@ type TestUser = {id: string; domain: string; clients: string[]};
36
36
  const user1: TestUser = {id: UUID.genV4().toString(), domain: '1.wire.test', clients: ['client1.1', 'client1.2']};
37
37
  const user2: TestUser = {id: UUID.genV4().toString(), domain: '2.wire.test', clients: ['client2.1', 'client2.2']};
38
38
 
39
- function generatePayload(users: TestUser[]): QualifiedOTRRecipients {
40
- const payload: QualifiedOTRRecipients = {};
39
+ function generateQualifiedRecipients(users: TestUser[]): QualifiedUserClients {
40
+ const payload: QualifiedUserClients = {};
41
41
  users.forEach(({id, domain, clients}) => {
42
42
  payload[domain] ||= {};
43
- payload[domain][id] = {};
44
- clients.forEach(client => (payload[domain][id][client] = new Uint8Array()));
43
+ payload[domain][id] = clients;
45
44
  });
46
45
  return payload;
47
46
  }
@@ -53,43 +52,112 @@ describe('MessageService', () => {
53
52
  describe('sendFederatedMessage', () => {
54
53
  it('sends a message', async () => {
55
54
  spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.returnValue(Promise.resolve(baseMessageSendingStatus));
56
- const recipients: QualifiedOTRRecipients = generatePayload([user1, user2]);
55
+ const recipients = generateQualifiedRecipients([user1, user2]);
57
56
 
58
- await messageService.sendFederatedOTRMessage('senderclientid', 'convid', '', recipients, new Uint8Array());
57
+ await messageService.sendFederatedOTRMessage(
58
+ 'senderclientid',
59
+ {id: 'convid', domain: ''},
60
+ recipients,
61
+ new Uint8Array(),
62
+ );
59
63
  expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalled();
60
64
  });
61
65
 
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) {
66
+ describe('client mismatch', () => {
67
+ it('handles client mismatch internally if no onClientMismatch is given', async () => {
68
+ let spyCounter = 0;
69
+ const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
70
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
71
+ spyCounter++;
72
+ if (spyCounter === 1) {
73
+ const error = new Error();
74
+ (error as any).response = {
75
+ status: StatusCodes.PRECONDITION_FAILED,
76
+ data: clientMismatch,
77
+ };
78
+ return Promise.reject(error);
79
+ }
80
+ return Promise.resolve(baseMessageSendingStatus);
81
+ });
82
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
83
+ spyOn(cryptographyService, 'encryptQualified').and.returnValue(
84
+ Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
85
+ );
86
+
87
+ const recipients = generateQualifiedRecipients([user1, user2]);
88
+
89
+ await messageService.sendFederatedOTRMessage(
90
+ 'senderclientid',
91
+ {id: 'convid', domain: ''},
92
+ recipients,
93
+ new Uint8Array(),
94
+ {reportMissing: true},
95
+ );
96
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
97
+ });
98
+
99
+ it('continues message sending if onClientMismatch returns true', async () => {
100
+ const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(true));
101
+ const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
102
+ let spyCounter = 0;
103
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
104
+ spyCounter++;
105
+ if (spyCounter === 1) {
106
+ const error = new Error();
107
+ (error as any).response = {
108
+ status: StatusCodes.PRECONDITION_FAILED,
109
+ data: clientMismatch,
110
+ };
111
+ return Promise.reject(error);
112
+ }
113
+ return Promise.resolve(baseMessageSendingStatus);
114
+ });
115
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
116
+ spyOn(cryptographyService, 'encryptQualified').and.returnValue(
117
+ Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
118
+ );
119
+
120
+ const recipients = generateQualifiedRecipients([user1, user2]);
121
+
122
+ await messageService.sendFederatedOTRMessage(
123
+ 'senderclientid',
124
+ {id: 'convid', domain: ''},
125
+ recipients,
126
+ new Uint8Array(),
127
+ {reportMissing: true, onClientMismatch},
128
+ );
129
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
130
+ expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
131
+ });
132
+
133
+ it('stops message sending if onClientMismatch returns false', async () => {
134
+ const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(false));
135
+ const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
136
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
67
137
  const error = new Error();
68
138
  (error as any).response = {
69
139
  status: StatusCodes.PRECONDITION_FAILED,
70
- data: {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}},
140
+ data: clientMismatch,
71
141
  };
72
142
  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
- );
143
+ });
144
+ spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
145
+ spyOn(cryptographyService, 'encryptQualified').and.returnValue(
146
+ Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
147
+ );
80
148
 
81
- const recipients: QualifiedOTRRecipients = generatePayload([user1, user2]);
149
+ const recipients = generateQualifiedRecipients([user1, user2]);
82
150
 
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);
151
+ await messageService.sendFederatedOTRMessage(
152
+ 'senderclientid',
153
+ {id: 'convid', domain: ''},
154
+ recipients,
155
+ new Uint8Array(),
156
+ {reportMissing: true, onClientMismatch},
157
+ );
158
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(1);
159
+ expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
160
+ });
93
161
  });
94
162
  });
95
163
  });
@@ -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
  );
@@ -148,14 +149,18 @@ export class MessageService {
148
149
 
149
150
  public async sendFederatedOTRMessage(
150
151
  sendingClientId: string,
151
- conversationId: string,
152
- conversationDomain: string,
153
- recipients: QualifiedOTRRecipients,
152
+ {id: conversationId, domain}: QualifiedId,
153
+ recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
154
154
  plainTextArray: Uint8Array,
155
- assetData?: Uint8Array,
156
- reportMissing?: boolean,
155
+ options: {
156
+ assetData?: Uint8Array;
157
+ reportMissing?: boolean;
158
+ onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
159
+ } = {},
157
160
  ): Promise<MessageSendingStatus> {
158
- const qualifiedUserEntries = Object.entries(recipients).map<ProtobufOTR.IQualifiedUserEntry>(
161
+ const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
162
+
163
+ const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
159
164
  ([domain, otrRecipients]) => {
160
165
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
161
166
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -186,8 +191,8 @@ export class MessageService {
186
191
  },
187
192
  });
188
193
 
189
- if (assetData) {
190
- protoMessage.blob = assetData;
194
+ if (options.assetData) {
195
+ protoMessage.blob = options.assetData;
191
196
  }
192
197
 
193
198
  /*
@@ -195,7 +200,7 @@ export class MessageService {
195
200
  * missing clients. We have to ignore missing clients because there can be the case that there are clients that
196
201
  * don't provide PreKeys (clients from the Pre-E2EE era).
197
202
  */
198
- if (reportMissing) {
203
+ if (options.reportMissing) {
199
204
  protoMessage.reportAll = {};
200
205
  } else {
201
206
  protoMessage.ignoreAll = {};
@@ -203,11 +208,7 @@ export class MessageService {
203
208
 
204
209
  let sendingStatus: MessageSendingStatus;
205
210
  try {
206
- sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(
207
- conversationId,
208
- conversationDomain,
209
- protoMessage,
210
- );
211
+ sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, protoMessage);
211
212
  } catch (error) {
212
213
  if (!this.isClientMismatchError(error)) {
213
214
  throw error;
@@ -218,8 +219,12 @@ export class MessageService {
218
219
  const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
219
220
 
220
221
  if (mismatch) {
221
- const reEncryptedMessage = await this.encryptForMissingClients(protoMessage, mismatch, plainTextArray);
222
- await this.apiClient.conversation.api.postOTRMessageV2(conversationId, conversationDomain, reEncryptedMessage);
222
+ const shouldStopSending = options.onClientMismatch && !(await options.onClientMismatch(mismatch));
223
+ if (shouldStopSending) {
224
+ return sendingStatus;
225
+ }
226
+ const reEncryptedMessage = await this.onFederatedMismatch(protoMessage, mismatch, plainTextArray);
227
+ await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, reEncryptedMessage);
223
228
  }
224
229
  return sendingStatus;
225
230
  }
@@ -282,7 +287,11 @@ export class MessageService {
282
287
  ignoreMissing,
283
288
  );
284
289
  } catch (error) {
285
- const reEncryptedMessage = await this.onClientProtobufMismatch(error as AxiosError, protoMessage, plainTextArray);
290
+ if (!this.isClientMismatchError(error)) {
291
+ throw error;
292
+ }
293
+ const mismatch = error.response!.data;
294
+ const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
286
295
  if (conversationId === null) {
287
296
  return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
288
297
  }
@@ -296,189 +305,187 @@ export class MessageService {
296
305
  }
297
306
 
298
307
  private async onClientMismatch(
299
- error: AxiosError,
308
+ clientMismatch: ClientMismatch,
300
309
  message: NewOTRMessage<Uint8Array>,
301
310
  plainTextArray: Uint8Array,
302
311
  ): 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
- }
312
+ const {missing, deleted} = clientMismatch;
313
+
314
+ const deletedUserIds = Object.keys(deleted);
315
+ const missingUserIds = Object.keys(missing);
316
+
317
+ if (deletedUserIds.length) {
318
+ for (const deletedUserId of deletedUserIds) {
319
+ for (const deletedClientId of deleted[deletedUserId]) {
320
+ const deletedUser = message.recipients[deletedUserId];
321
+ if (deletedUser) {
322
+ delete deletedUser[deletedClientId];
316
323
  }
317
324
  }
318
325
  }
326
+ }
319
327
 
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];
328
+ if (missingUserIds.length) {
329
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
330
+ const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
331
+ for (const missingUserId of missingUserIds) {
332
+ for (const missingClientId in reEncryptedPayloads[missingUserId]) {
333
+ const missingUser = message.recipients[missingUserId];
334
+ if (!missingUser) {
335
+ message.recipients[missingUserId] = {};
331
336
  }
337
+
338
+ message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
332
339
  }
333
340
  }
334
-
335
- return message;
336
341
  }
337
342
 
338
- throw error;
343
+ return message;
339
344
  }
340
345
 
341
346
  private async onClientProtobufMismatch(
342
- error: AxiosError,
347
+ clientMismatch: {missing: UserClients; deleted: UserClients},
343
348
  message: ProtobufOTR.NewOtrMessage,
344
349
  plainTextArray: Uint8Array,
345
350
  ): 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
- }
351
+ const {missing, deleted} = clientMismatch;
352
+
353
+ const deletedUserIds = Object.keys(deleted);
354
+ const missingUserIds = Object.keys(missing);
355
+
356
+ if (deletedUserIds.length) {
357
+ for (const deletedUserId of deletedUserIds) {
358
+ for (const deletedClientId of deleted[deletedUserId]) {
359
+ const deletedUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === deletedUserId);
360
+ if (deletedUserIndex > -1) {
361
+ const deletedClientIndex = message.recipients[deletedUserIndex].clients?.findIndex(({client}) => {
362
+ return client.client.toString(16) === deletedClientId;
363
+ });
364
+ if (typeof deletedClientIndex !== 'undefined' && deletedClientIndex > -1) {
365
+ delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
363
366
  }
364
367
  }
365
368
  }
366
369
  }
370
+ }
367
371
 
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],
372
+ if (missingUserIds.length) {
373
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
374
+ const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
375
+ for (const missingUserId of missingUserIds) {
376
+ for (const missingClientId in reEncryptedPayloads[missingUserId]) {
377
+ const missingUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
378
+ if (missingUserIndex === -1) {
379
+ message.recipients.push({
380
+ clients: [
381
+ {
382
+ client: {
383
+ client: Long.fromString(missingClientId, 16),
382
384
  },
383
- ],
384
- user: {
385
- uuid: uuidToBytes(missingUserId),
385
+ text: reEncryptedPayloads[missingUserId][missingClientId],
386
386
  },
387
- });
388
- }
387
+ ],
388
+ user: {
389
+ uuid: uuidToBytes(missingUserId),
390
+ },
391
+ });
389
392
  }
390
393
  }
391
394
  }
392
-
393
- return message;
394
395
  }
395
396
 
396
- throw error;
397
+ return message;
397
398
  }
398
399
 
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> {
400
+ private deleteExtraQualifiedClients(
401
+ message: ProtobufOTR.QualifiedNewOtrMessage,
402
+ deletedClients: MessageSendingStatus['deleted'],
403
+ ): ProtobufOTR.QualifiedNewOtrMessage {
412
404
  // 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)) {
405
+ for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
406
+ if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
415
407
  // no user from this domain was deleted
416
408
  continue;
417
409
  }
418
410
  // walk through deleted user ids
419
411
  for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
420
412
  // walk through message recipients
421
- for (const recipientIndex in messageData.recipients) {
413
+ for (const recipientIndex in message.recipients) {
422
414
  // check if message recipients' domain is the same as the deleted user's domain
423
- if (messageData.recipients[recipientIndex].domain === deletedUserDomain) {
415
+ if (message.recipients[recipientIndex].domain === deletedUserDomain) {
424
416
  // 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;
417
+ for (const entriesIndex in message.recipients[recipientIndex].entries || []) {
418
+ const uuid = message.recipients[recipientIndex].entries![entriesIndex].user?.uuid;
427
419
  if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
428
420
  // delete this user from the message recipients
429
- delete messageData.recipients[recipientIndex].entries![entriesIndex];
421
+ delete message.recipients[recipientIndex].entries![entriesIndex];
430
422
  }
431
423
  }
432
424
  }
433
425
  }
434
426
  }
435
427
  }
428
+ return message;
429
+ }
436
430
 
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);
431
+ /**
432
+ * 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)
433
+ *
434
+ * @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
435
+ * @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
436
+ * @param {Uint8Array} plainText The text that should be encrypted for the missing clients
437
+ * @return resolves with a new message payload that can be sent
438
+ */
439
+ private async onFederatedMismatch(
440
+ message: ProtobufOTR.QualifiedNewOtrMessage,
441
+ {deleted, missing}: MessageSendingStatus,
442
+ plainText: Uint8Array,
443
+ ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
444
+ message = this.deleteExtraQualifiedClients(message, deleted);
445
+ if (Object.keys(missing).length) {
446
+ const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(missing);
447
+ const reEncryptedPayloads = await this.cryptographyService.encryptQualified(plainText, missingPreKeyBundles);
448
+ message = this.addMissingQualifiedClients(message, reEncryptedPayloads);
449
+ }
450
+ return message;
451
+ }
443
452
 
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
- }
453
+ private addMissingQualifiedClients(
454
+ messageData: ProtobufOTR.QualifiedNewOtrMessage,
455
+ reEncryptedPayloads: QualifiedOTRRecipients,
456
+ ): ProtobufOTR.QualifiedNewOtrMessage {
457
+ // walk through missing domain/user map
458
+ for (const [missingDomain, userClients] of Object.entries(reEncryptedPayloads)) {
459
+ // walk through missing user ids
460
+ for (const [missingUserId, missingClientIds] of Object.entries(userClients)) {
461
+ // walk through message recipients
462
+ for (const domain of messageData.recipients) {
463
+ // check if message recipients' domain is the same as the missing user's domain
464
+ if (domain.domain === missingDomain) {
465
+ // check if there is a recipient with same user id as the missing user's id
466
+ let userIndex = domain.entries?.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
467
+
468
+ if (userIndex === -1) {
469
+ // no recipient found, let's create it
470
+ userIndex =
471
+ domain.entries!.push({
472
+ user: {
473
+ uuid: uuidToBytes(missingUserId),
474
+ },
475
+ }) - 1;
476
+ }
450
477
 
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
- }
478
+ const missingUserUUID = domain.entries![userIndex!].user.uuid;
469
479
 
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
- }
480
+ if (bytesToUUID(missingUserUUID) === missingUserId) {
481
+ for (const [missingClientId, missingClientPayload] of Object.entries(missingClientIds)) {
482
+ domain.entries![userIndex!].clients ||= [];
483
+ domain.entries![userIndex!].clients?.push({
484
+ client: {
485
+ client: Long.fromString(missingClientId, 16),
486
+ },
487
+ text: missingClientPayload,
488
+ });
482
489
  }
483
490
  }
484
491
  }
@@ -1,6 +1,7 @@
1
1
  import type { QualifiedUserClients, UserClients } from '@wireapp/api-client/src/conversation/';
2
2
  import type { QualifiedId } from '@wireapp/api-client/src/user/';
3
3
  export declare function isStringArray(obj: any): obj is string[];
4
+ export declare function isQualifiedId(obj: any): obj is QualifiedId;
4
5
  export declare function isQualifiedIdArray(obj: any): obj is QualifiedId[];
5
6
  export declare function isQualifiedUserClients(obj: any): obj is QualifiedUserClients;
6
7
  export declare function isUserClients(obj: any): obj is UserClients;
@@ -18,13 +18,17 @@
18
18
  *
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.isUserClients = exports.isQualifiedUserClients = exports.isQualifiedIdArray = exports.isStringArray = void 0;
21
+ exports.isUserClients = exports.isQualifiedUserClients = exports.isQualifiedIdArray = exports.isQualifiedId = exports.isStringArray = void 0;
22
22
  function isStringArray(obj) {
23
23
  return Array.isArray(obj) && typeof obj[0] === 'string';
24
24
  }
25
25
  exports.isStringArray = isStringArray;
26
+ function isQualifiedId(obj) {
27
+ return typeof obj === 'object' && typeof obj['domain'] === 'string';
28
+ }
29
+ exports.isQualifiedId = isQualifiedId;
26
30
  function isQualifiedIdArray(obj) {
27
- return Array.isArray(obj) && typeof obj[0] === 'object' && typeof obj[0]['domain'] === 'string';
31
+ return Array.isArray(obj) && isQualifiedId(obj[0]);
28
32
  }
29
33
  exports.isQualifiedIdArray = isQualifiedIdArray;
30
34
  function isQualifiedUserClients(obj) {
@@ -1 +1 @@
1
- {"version":3,"file":"TypePredicateUtil.js","sourceRoot":"","sources":["TypePredicateUtil.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAKH,SAAgB,aAAa,CAAC,GAAQ;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;AAC1D,CAAC;AAFD,sCAEC;AAED,SAAgB,kBAAkB,CAAC,GAAQ;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC;AAClG,CAAC;AAFD,gDAEC;AAED,SAAgB,sBAAsB,CAAC,GAAQ;;IAC7C,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE;QAC3B,MAAM,qBAAqB,GAAG,MAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,0CAAG,CAAC,CAAC,CAAC;QACtD,IAAI,OAAO,qBAAqB,KAAK,QAAQ,EAAE;YAC7C,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,CAAC,qBAA+B,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7E,IAAI,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE;gBACrC,MAAM,aAAa,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;gBAC5C,OAAO,OAAO,aAAa,KAAK,QAAQ,IAAI,OAAO,aAAa,KAAK,WAAW,CAAC;aAClF;SACF;KACF;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAZD,wDAYC;AAED,SAAgB,aAAa,CAAC,GAAQ;;IACpC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE;QAC3B,MAAM,oBAAoB,GAAG,MAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,0CAAG,CAAC,CAAC,CAAC;QACrD,IAAI,KAAK,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE;YACvC,MAAM,aAAa,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;YAC9C,OAAO,OAAO,aAAa,KAAK,QAAQ,CAAC;SAC1C;KACF;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AATD,sCASC"}
1
+ {"version":3,"file":"TypePredicateUtil.js","sourceRoot":"","sources":["TypePredicateUtil.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAKH,SAAgB,aAAa,CAAC,GAAQ;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;AAC1D,CAAC;AAFD,sCAEC;AAED,SAAgB,aAAa,CAAC,GAAQ;IACpC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC;AACtE,CAAC;AAFD,sCAEC;AAED,SAAgB,kBAAkB,CAAC,GAAQ;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAFD,gDAEC;AAED,SAAgB,sBAAsB,CAAC,GAAQ;;IAC7C,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE;QAC3B,MAAM,qBAAqB,GAAG,MAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,0CAAG,CAAC,CAAC,CAAC;QACtD,IAAI,OAAO,qBAAqB,KAAK,QAAQ,EAAE;YAC7C,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,CAAC,qBAA+B,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7E,IAAI,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE;gBACrC,MAAM,aAAa,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;gBAC5C,OAAO,OAAO,aAAa,KAAK,QAAQ,IAAI,OAAO,aAAa,KAAK,WAAW,CAAC;aAClF;SACF;KACF;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAZD,wDAYC;AAED,SAAgB,aAAa,CAAC,GAAQ;;IACpC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE;QAC3B,MAAM,oBAAoB,GAAG,MAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,0CAAG,CAAC,CAAC,CAAC;QACrD,IAAI,KAAK,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE;YACvC,MAAM,aAAa,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;YAC9C,OAAO,OAAO,aAAa,KAAK,QAAQ,CAAC;SAC1C;KACF;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AATD,sCASC"}
@@ -24,8 +24,12 @@ export function isStringArray(obj: any): obj is string[] {
24
24
  return Array.isArray(obj) && typeof obj[0] === 'string';
25
25
  }
26
26
 
27
+ export function isQualifiedId(obj: any): obj is QualifiedId {
28
+ return typeof obj === 'object' && typeof obj['domain'] === 'string';
29
+ }
30
+
27
31
  export function isQualifiedIdArray(obj: any): obj is QualifiedId[] {
28
- return Array.isArray(obj) && typeof obj[0] === 'object' && typeof obj[0]['domain'] === 'string';
32
+ return Array.isArray(obj) && isQualifiedId(obj[0]);
29
33
  }
30
34
 
31
35
  export function isQualifiedUserClients(obj: any): obj is QualifiedUserClients {