@wireapp/core 17.20.2 → 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.
Files changed (26) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +3 -3
  3. package/src/main/conversation/ConversationService.d.ts +39 -5
  4. package/src/main/conversation/ConversationService.js +95 -38
  5. package/src/main/conversation/ConversationService.js.map +1 -1
  6. package/src/main/conversation/ConversationService.ts +110 -76
  7. package/src/main/conversation/message/MessageService.d.ts +19 -3
  8. package/src/main/conversation/message/MessageService.js +155 -119
  9. package/src/main/conversation/message/MessageService.js.map +1 -1
  10. package/src/main/conversation/message/MessageService.test.node.d.ts +1 -0
  11. package/src/main/conversation/message/MessageService.test.node.js +123 -0
  12. package/src/main/conversation/message/MessageService.test.node.js.map +1 -0
  13. package/src/main/conversation/message/MessageService.test.node.ts +163 -0
  14. package/src/main/conversation/message/MessageService.ts +176 -149
  15. package/src/main/cryptography/CryptographyService.d.ts +3 -3
  16. package/src/main/cryptography/CryptographyService.js +14 -11
  17. package/src/main/cryptography/CryptographyService.js.map +1 -1
  18. package/src/main/cryptography/CryptographyService.ts +23 -16
  19. package/src/main/util/TypePredicateUtil.d.ts +1 -0
  20. package/src/main/util/TypePredicateUtil.js +7 -3
  21. package/src/main/util/TypePredicateUtil.js.map +1 -1
  22. package/src/main/util/TypePredicateUtil.test.node.d.ts +1 -0
  23. package/src/main/util/TypePredicateUtil.test.node.js +42 -0
  24. package/src/main/util/TypePredicateUtil.test.node.js.map +1 -0
  25. package/src/main/util/TypePredicateUtil.test.node.ts +44 -0
  26. package/src/main/util/TypePredicateUtil.ts +6 -2
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Wire
3
+ * Copyright (C) 2021 Wire Swiss GmbH
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU General Public License
16
+ * along with this program. If not, see http://www.gnu.org/licenses/.
17
+ *
18
+ */
19
+
20
+ import UUID from 'uuidjs';
21
+ import {StatusCodes} from 'http-status-codes';
22
+ import {APIClient} from '@wireapp/api-client';
23
+ import {MessageSendingStatus, QualifiedUserClients} from '@wireapp/api-client/src/conversation';
24
+ import {CryptographyService} from '../../cryptography';
25
+ import {MessageService} from './MessageService';
26
+
27
+ const baseMessageSendingStatus: MessageSendingStatus = {
28
+ deleted: {},
29
+ missing: {},
30
+ failed_to_send: {},
31
+ redundant: {},
32
+ time: new Date().toISOString(),
33
+ };
34
+
35
+ type TestUser = {id: string; domain: string; clients: string[]};
36
+ const user1: TestUser = {id: UUID.genV4().toString(), domain: '1.wire.test', clients: ['client1.1', 'client1.2']};
37
+ const user2: TestUser = {id: UUID.genV4().toString(), domain: '2.wire.test', clients: ['client2.1', 'client2.2']};
38
+
39
+ function generateQualifiedRecipients(users: TestUser[]): QualifiedUserClients {
40
+ const payload: QualifiedUserClients = {};
41
+ users.forEach(({id, domain, clients}) => {
42
+ payload[domain] ||= {};
43
+ payload[domain][id] = clients;
44
+ });
45
+ return payload;
46
+ }
47
+
48
+ describe('MessageService', () => {
49
+ const apiClient = new APIClient();
50
+ const cryptographyService = new CryptographyService(apiClient, {} as any);
51
+ const messageService = new MessageService(apiClient, cryptographyService);
52
+ describe('sendFederatedMessage', () => {
53
+ it('sends a message', async () => {
54
+ spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.returnValue(Promise.resolve(baseMessageSendingStatus));
55
+ const recipients = generateQualifiedRecipients([user1, user2]);
56
+
57
+ await messageService.sendFederatedOTRMessage(
58
+ 'senderclientid',
59
+ {id: 'convid', domain: ''},
60
+ recipients,
61
+ new Uint8Array(),
62
+ );
63
+ expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalled();
64
+ });
65
+
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(() => {
137
+ const error = new Error();
138
+ (error as any).response = {
139
+ status: StatusCodes.PRECONDITION_FAILED,
140
+ data: clientMismatch,
141
+ };
142
+ return Promise.reject(error);
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
+ );
148
+
149
+ const recipients = generateQualifiedRecipients([user1, user2]);
150
+
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
+ });
161
+ });
162
+ });
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
  );
@@ -88,6 +89,10 @@ export class MessageService {
88
89
  }
89
90
  }
90
91
 
92
+ private isClientMismatchError(error: any): error is ClientMismatchError {
93
+ return error.response?.status === HTTP_STATUS.PRECONDITION_FAILED;
94
+ }
95
+
91
96
  private checkFederatedClientsMismatch(
92
97
  messageData: ProtobufOTR.QualifiedNewOtrMessage,
93
98
  messageSendingStatus: MessageSendingStatus,
@@ -144,13 +149,18 @@ export class MessageService {
144
149
 
145
150
  public async sendFederatedOTRMessage(
146
151
  sendingClientId: string,
147
- conversationId: string,
148
- conversationDomain: string,
149
- recipients: QualifiedOTRRecipients,
152
+ {id: conversationId, domain}: QualifiedId,
153
+ recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
150
154
  plainTextArray: Uint8Array,
151
- assetData?: Uint8Array,
155
+ options: {
156
+ assetData?: Uint8Array;
157
+ reportMissing?: boolean;
158
+ onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
159
+ } = {},
152
160
  ): Promise<MessageSendingStatus> {
153
- 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>(
154
164
  ([domain, otrRecipients]) => {
155
165
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
156
166
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -181,8 +191,8 @@ export class MessageService {
181
191
  },
182
192
  });
183
193
 
184
- if (assetData) {
185
- protoMessage.blob = assetData;
194
+ if (options.assetData) {
195
+ protoMessage.blob = options.assetData;
186
196
  }
187
197
 
188
198
  /*
@@ -190,25 +200,33 @@ export class MessageService {
190
200
  * missing clients. We have to ignore missing clients because there can be the case that there are clients that
191
201
  * don't provide PreKeys (clients from the Pre-E2EE era).
192
202
  */
193
- protoMessage.ignoreAll = {};
203
+ if (options.reportMissing) {
204
+ protoMessage.reportAll = {};
205
+ } else {
206
+ protoMessage.ignoreAll = {};
207
+ }
194
208
 
195
- const messageSendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(
196
- conversationId,
197
- conversationDomain,
198
- protoMessage,
199
- );
209
+ let sendingStatus: MessageSendingStatus;
210
+ try {
211
+ sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, protoMessage);
212
+ } catch (error) {
213
+ if (!this.isClientMismatchError(error)) {
214
+ throw error;
215
+ }
216
+ sendingStatus = error.response!.data! as unknown as MessageSendingStatus;
217
+ }
200
218
 
201
- const federatedClientsMismatch = this.checkFederatedClientsMismatch(protoMessage, messageSendingStatus);
219
+ const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
202
220
 
203
- if (federatedClientsMismatch) {
204
- const reEncryptedMessage = await this.onFederatedClientMismatch(
205
- protoMessage,
206
- federatedClientsMismatch,
207
- plainTextArray,
208
- );
209
- await this.apiClient.conversation.api.postOTRMessageV2(conversationId, conversationDomain, reEncryptedMessage);
221
+ if (mismatch) {
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);
210
228
  }
211
- return messageSendingStatus;
229
+ return sendingStatus;
212
230
  }
213
231
 
214
232
  public async sendOTRProtobufMessage(
@@ -269,7 +287,11 @@ export class MessageService {
269
287
  ignoreMissing,
270
288
  );
271
289
  } catch (error) {
272
- 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);
273
295
  if (conversationId === null) {
274
296
  return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
275
297
  }
@@ -283,182 +305,187 @@ export class MessageService {
283
305
  }
284
306
 
285
307
  private async onClientMismatch(
286
- error: AxiosError,
308
+ clientMismatch: ClientMismatch,
287
309
  message: NewOTRMessage<Uint8Array>,
288
310
  plainTextArray: Uint8Array,
289
311
  ): Promise<NewOTRMessage<Uint8Array>> {
290
- if (error.response?.status === HTTP_STATUS.PRECONDITION_FAILED) {
291
- const {missing, deleted} = (error as ClientMismatchError).response?.data!;
292
-
293
- const deletedUserIds = Object.keys(deleted);
294
- const missingUserIds = Object.keys(missing);
295
-
296
- if (deletedUserIds.length) {
297
- for (const deletedUserId of deletedUserIds) {
298
- for (const deletedClientId of deleted[deletedUserId]) {
299
- const deletedUser = message.recipients[deletedUserId];
300
- if (deletedUser) {
301
- delete deletedUser[deletedClientId];
302
- }
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];
303
323
  }
304
324
  }
305
325
  }
326
+ }
306
327
 
307
- if (missingUserIds.length) {
308
- const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
309
- const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
310
- for (const missingUserId of missingUserIds) {
311
- for (const missingClientId in reEncryptedPayloads[missingUserId]) {
312
- const missingUser = message.recipients[missingUserId];
313
- if (!missingUser) {
314
- message.recipients[missingUserId] = {};
315
- }
316
-
317
- 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] = {};
318
336
  }
337
+
338
+ message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
319
339
  }
320
340
  }
321
-
322
- return message;
323
341
  }
324
342
 
325
- throw error;
343
+ return message;
326
344
  }
327
345
 
328
346
  private async onClientProtobufMismatch(
329
- error: AxiosError,
347
+ clientMismatch: {missing: UserClients; deleted: UserClients},
330
348
  message: ProtobufOTR.NewOtrMessage,
331
349
  plainTextArray: Uint8Array,
332
350
  ): Promise<ProtobufOTR.NewOtrMessage> {
333
- if (error.response?.status === HTTP_STATUS.PRECONDITION_FAILED) {
334
- const {missing, deleted} = (error as ClientMismatchError).response?.data!;
335
-
336
- const deletedUserIds = Object.keys(deleted);
337
- const missingUserIds = Object.keys(missing);
338
-
339
- if (deletedUserIds.length) {
340
- for (const deletedUserId of deletedUserIds) {
341
- for (const deletedClientId of deleted[deletedUserId]) {
342
- const deletedUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === deletedUserId);
343
- if (deletedUserIndex > -1) {
344
- const deletedClientIndex = message.recipients[deletedUserIndex].clients?.findIndex(({client}) => {
345
- return client.client.toString(16) === deletedClientId;
346
- });
347
- if (typeof deletedClientIndex !== 'undefined' && deletedClientIndex > -1) {
348
- delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
349
- }
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!];
350
366
  }
351
367
  }
352
368
  }
353
369
  }
370
+ }
354
371
 
355
- if (missingUserIds.length) {
356
- const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(missing);
357
- const reEncryptedPayloads = await this.cryptographyService.encrypt(plainTextArray, missingPreKeyBundles);
358
- for (const missingUserId of missingUserIds) {
359
- for (const missingClientId in reEncryptedPayloads[missingUserId]) {
360
- const missingUserIndex = message.recipients.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
361
- if (missingUserIndex === -1) {
362
- message.recipients.push({
363
- clients: [
364
- {
365
- client: {
366
- client: Long.fromString(missingClientId, 16),
367
- },
368
- 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),
369
384
  },
370
- ],
371
- user: {
372
- uuid: uuidToBytes(missingUserId),
385
+ text: reEncryptedPayloads[missingUserId][missingClientId],
373
386
  },
374
- });
375
- }
387
+ ],
388
+ user: {
389
+ uuid: uuidToBytes(missingUserId),
390
+ },
391
+ });
376
392
  }
377
393
  }
378
394
  }
379
-
380
- return message;
381
395
  }
382
396
 
383
- throw error;
397
+ return message;
384
398
  }
385
399
 
386
- private async onFederatedClientMismatch(
387
- messageData: ProtobufOTR.QualifiedNewOtrMessage,
388
- messageSendingStatus: MessageSendingStatus,
389
- plainTextArray: Uint8Array,
390
- ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
400
+ private deleteExtraQualifiedClients(
401
+ message: ProtobufOTR.QualifiedNewOtrMessage,
402
+ deletedClients: MessageSendingStatus['deleted'],
403
+ ): ProtobufOTR.QualifiedNewOtrMessage {
391
404
  // walk through deleted domain/user map
392
- for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(messageSendingStatus.deleted)) {
393
- 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)) {
394
407
  // no user from this domain was deleted
395
408
  continue;
396
409
  }
397
410
  // walk through deleted user ids
398
411
  for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
399
412
  // walk through message recipients
400
- for (const recipientIndex in messageData.recipients) {
413
+ for (const recipientIndex in message.recipients) {
401
414
  // check if message recipients' domain is the same as the deleted user's domain
402
- if (messageData.recipients[recipientIndex].domain === deletedUserDomain) {
415
+ if (message.recipients[recipientIndex].domain === deletedUserDomain) {
403
416
  // check if message recipients' id is the same as the deleted user's id
404
- for (const entriesIndex in messageData.recipients[recipientIndex].entries || []) {
405
- 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;
406
419
  if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
407
420
  // delete this user from the message recipients
408
- delete messageData.recipients[recipientIndex].entries![entriesIndex];
421
+ delete message.recipients[recipientIndex].entries![entriesIndex];
409
422
  }
410
423
  }
411
424
  }
412
425
  }
413
426
  }
414
427
  }
428
+ return message;
429
+ }
415
430
 
416
- const missingUserIds = Object.entries(messageSendingStatus.missing);
417
- if (missingUserIds.length) {
418
- const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(
419
- messageSendingStatus.missing,
420
- );
421
- const reEncryptedPayloads = await this.cryptographyService.encryptQualified(plainTextArray, missingPreKeyBundles);
422
-
423
- // walk through missing domain/user map
424
- for (const [missingUserDomain, missingUserIdClients] of missingUserIds) {
425
- if (!messageData.recipients.find(recipient => recipient.domain === missingUserDomain)) {
426
- // no user from this domain is missing
427
- continue;
428
- }
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
+ }
429
452
 
430
- // walk through missing user ids
431
- for (const [missingUserId, missingClientIds] of Object.entries(missingUserIdClients)) {
432
- // walk through message recipients
433
- for (const recipientIndex in messageData.recipients) {
434
- // check if message recipients' domain is the same as the missing user's domain
435
- if (messageData.recipients[recipientIndex].domain === missingUserDomain) {
436
- // check if there is a recipient with same user id as the missing user's id
437
- let userIndex = messageData.recipients[recipientIndex].entries?.findIndex(
438
- ({user}) => bytesToUUID(user.uuid) === missingUserId,
439
- );
440
-
441
- if (userIndex === -1) {
442
- // no recipient found, let's create it
443
- userIndex = messageData.recipients[recipientIndex].entries!.push({
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({
444
472
  user: {
445
473
  uuid: uuidToBytes(missingUserId),
446
474
  },
447
- });
448
- }
475
+ }) - 1;
476
+ }
449
477
 
450
- const missingUserUUID = messageData.recipients[recipientIndex].entries![userIndex!].user.uuid;
451
-
452
- if (bytesToUUID(missingUserUUID) === missingUserId) {
453
- for (const missingClientId of missingClientIds) {
454
- messageData.recipients[recipientIndex].entries![userIndex!].clients ||= [];
455
- messageData.recipients[recipientIndex].entries![userIndex!].clients?.push({
456
- client: {
457
- client: Long.fromString(missingClientId, 16),
458
- },
459
- text: reEncryptedPayloads[missingUserDomain][missingUserId][missingClientId],
460
- });
461
- }
478
+ const missingUserUUID = domain.entries![userIndex!].user.uuid;
479
+
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
+ });
462
489
  }
463
490
  }
464
491
  }
@@ -1,7 +1,7 @@
1
1
  import type { APIClient } from '@wireapp/api-client';
2
2
  import type { PreKey as SerializedPreKey } from '@wireapp/api-client/src/auth/';
3
3
  import type { RegisteredClient } from '@wireapp/api-client/src/client/';
4
- import type { OTRRecipients, QualifiedOTRRecipients } from '@wireapp/api-client/src/conversation/';
4
+ import type { OTRRecipients, QualifiedOTRRecipients, QualifiedUserClients, UserClients } from '@wireapp/api-client/src/conversation/';
5
5
  import type { ConversationOtrMessageAddEvent } from '@wireapp/api-client/src/event';
6
6
  import type { QualifiedUserPreKeyBundleMap, UserPreKeyBundleMap } from '@wireapp/api-client/src/user/';
7
7
  import { Cryptobox } from '@wireapp/cryptobox';
@@ -26,8 +26,8 @@ export declare class CryptographyService {
26
26
  createCryptobox(): Promise<SerializedPreKey[]>;
27
27
  decrypt(sessionId: string, encodedCiphertext: string): Promise<Uint8Array>;
28
28
  private static dismantleSessionId;
29
- encryptQualified(plainText: Uint8Array, preKeyBundles: QualifiedUserPreKeyBundleMap): Promise<QualifiedOTRRecipients>;
30
- encrypt(plainText: Uint8Array, preKeyBundles: UserPreKeyBundleMap, domain?: string): Promise<OTRRecipients<Uint8Array>>;
29
+ encryptQualified(plainText: Uint8Array, preKeyBundles: QualifiedUserPreKeyBundleMap | QualifiedUserClients): Promise<QualifiedOTRRecipients>;
30
+ encrypt(plainText: Uint8Array, users: UserPreKeyBundleMap | UserClients, domain?: string): Promise<OTRRecipients<Uint8Array>>;
31
31
  private encryptPayloadForSession;
32
32
  initCryptobox(): Promise<void>;
33
33
  deleteCryptographyStores(): Promise<boolean[]>;