@wireapp/core 17.23.0 → 17.25.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 (41) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +2 -2
  3. package/src/main/Account.d.ts +10 -1
  4. package/src/main/Account.js +8 -2
  5. package/src/main/Account.js.map +1 -1
  6. package/src/main/Account.ts +15 -2
  7. package/src/main/broadcast/BroadcastService.js +3 -4
  8. package/src/main/broadcast/BroadcastService.js.map +1 -1
  9. package/src/main/broadcast/BroadcastService.ts +3 -5
  10. package/src/main/conversation/ConversationService.d.ts +7 -8
  11. package/src/main/conversation/ConversationService.js +32 -61
  12. package/src/main/conversation/ConversationService.js.map +1 -1
  13. package/src/main/conversation/ConversationService.test.node.js +17 -38
  14. package/src/main/conversation/ConversationService.test.node.js.map +1 -1
  15. package/src/main/conversation/ConversationService.test.node.ts +20 -50
  16. package/src/main/conversation/ConversationService.ts +40 -119
  17. package/src/main/conversation/message/MessageService.d.ts +42 -12
  18. package/src/main/conversation/message/MessageService.js +146 -281
  19. package/src/main/conversation/message/MessageService.js.map +1 -1
  20. package/src/main/conversation/message/MessageService.test.node.js +100 -6
  21. package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
  22. package/src/main/conversation/message/MessageService.test.node.ts +153 -29
  23. package/src/main/conversation/message/MessageService.ts +208 -360
  24. package/src/main/conversation/message/UserClientsUtil.d.ts +22 -0
  25. package/src/main/conversation/message/UserClientsUtil.js +38 -0
  26. package/src/main/conversation/message/UserClientsUtil.js.map +1 -0
  27. package/src/main/conversation/message/UserClientsUtil.ts +44 -0
  28. package/src/main/conversation/message/UserClientsUtils.test.node.d.ts +1 -0
  29. package/src/main/conversation/message/UserClientsUtils.test.node.js +42 -0
  30. package/src/main/conversation/message/UserClientsUtils.test.node.js.map +1 -0
  31. package/src/main/conversation/message/UserClientsUtils.test.node.ts +44 -0
  32. package/src/main/cryptography/CryptographyService.d.ts +6 -1
  33. package/src/main/cryptography/CryptographyService.js +14 -2
  34. package/src/main/cryptography/CryptographyService.js.map +1 -1
  35. package/src/main/cryptography/CryptographyService.test.node.js +2 -2
  36. package/src/main/cryptography/CryptographyService.test.node.js.map +1 -1
  37. package/src/main/cryptography/CryptographyService.test.node.ts +5 -4
  38. package/src/main/cryptography/CryptographyService.ts +22 -4
  39. package/src/main/util/TypePredicateUtil.js +3 -9
  40. package/src/main/util/TypePredicateUtil.js.map +1 -1
  41. package/src/main/util/TypePredicateUtil.ts +3 -9
@@ -21,7 +21,7 @@ import {StatusCodes as HTTP_STATUS} from 'http-status-codes';
21
21
  import {AxiosError} from 'axios';
22
22
  import {proteus as ProtobufOTR} from '@wireapp/protocol-messaging/web/otr';
23
23
  import Long from 'long';
24
- import {bytesToUUID, uuidToBytes} from '@wireapp/commons/src/main/util/StringUtil';
24
+ import {uuidToBytes} from '@wireapp/commons/src/main/util/StringUtil';
25
25
  import {APIClient} from '@wireapp/api-client';
26
26
  import {
27
27
  ClientMismatch,
@@ -32,139 +32,124 @@ import {
32
32
  QualifiedUserClients,
33
33
  UserClients,
34
34
  } from '@wireapp/api-client/src/conversation';
35
- import {Decoder, Encoder} from 'bazinga64';
35
+ import {Encoder} from 'bazinga64';
36
36
 
37
+ import {encryptAsset} from '../../cryptography/AssetCryptography.node';
37
38
  import {CryptographyService} from '../../cryptography';
38
- import {QualifiedId, QualifiedUserPreKeyBundleMap} from '@wireapp/api-client/src/user';
39
+ import {QualifiedId, QualifiedUserPreKeyBundleMap, UserPreKeyBundleMap} from '@wireapp/api-client/src/user';
40
+ import {MessageBuilder} from './MessageBuilder';
41
+ import {GenericMessage} from '@wireapp/protocol-messaging';
42
+ import {GenericMessageType} from '..';
43
+ import {flattenUserClients, flattenQualifiedUserClients} from './UserClientsUtil';
39
44
 
40
- type ClientMismatchError = AxiosError<ClientMismatch>;
45
+ type ClientMismatchError = AxiosError<ClientMismatch | MessageSendingStatus>;
41
46
 
42
47
  export class MessageService {
43
48
  constructor(private readonly apiClient: APIClient, private readonly cryptographyService: CryptographyService) {}
44
49
 
45
- public async sendOTRMessage(
50
+ /**
51
+ * Sends a message to a non-federated backend.
52
+ *
53
+ * @param sendingClientId The clientId of the current user
54
+ * @param recipients The list of recipients to send the message to
55
+ * @param plainText The plainText data to send
56
+ * @param options.conversationId? the conversation to send the message to. Will broadcast if not set
57
+ * @param options.reportMissing? trigger a mismatch error when there are missing recipients in the payload
58
+ * @param options.sendAsProtobuf?
59
+ * @param options.onClientMismatch? Called when a mismatch happens on the server
60
+ * @return the ClientMismatch status returned by the backend
61
+ */
62
+ public async sendMessage(
46
63
  sendingClientId: string,
47
- recipients: OTRRecipients<Uint8Array>,
48
- conversationId: string | null,
49
- plainTextArray: Uint8Array,
50
- base64CipherText?: string,
64
+ recipients: UserClients | UserPreKeyBundleMap,
65
+ plainText: Uint8Array,
66
+ options: {
67
+ conversationId?: string;
68
+ reportMissing?: boolean;
69
+ sendAsProtobuf?: boolean;
70
+ onClientMismatch?: (mismatch: ClientMismatch) => Promise<boolean | undefined>;
71
+ } = {},
51
72
  ): Promise<ClientMismatch> {
52
- const message: NewOTRMessage<string> = {
53
- data: base64CipherText,
54
- recipients: CryptographyService.convertArrayRecipientsToBase64(recipients),
55
- sender: sendingClientId,
56
- };
57
-
58
- /*
59
- * When creating the PreKey bundles we already found out to which users we want to send a message, so we can ignore
60
- * missing clients. We have to ignore missing clients because there can be the case that there are clients that
61
- * don't provide PreKeys (clients from the Pre-E2EE era).
62
- */
63
- const ignoreMissing = true;
73
+ let plainTextPayload = plainText;
74
+ let cipherText: Uint8Array;
75
+ if (this.shouldSendAsExternal(plainText, recipients)) {
76
+ const externalPayload = await this.generateExternalPayload(plainText);
77
+ plainTextPayload = externalPayload.text;
78
+ cipherText = externalPayload.cipherText;
79
+ }
64
80
 
81
+ const encryptedPayload = await this.cryptographyService.encrypt(plainTextPayload, recipients);
82
+ const send = (payload: OTRRecipients<Uint8Array>) => {
83
+ return options.sendAsProtobuf
84
+ ? this.sendOTRProtobufMessage(sendingClientId, payload, {...options, assetData: cipherText})
85
+ : this.sendOTRMessage(sendingClientId, payload, {...options, assetData: cipherText});
86
+ };
65
87
  try {
66
- if (conversationId === null) {
67
- return await this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, message, ignoreMissing);
68
- }
69
- return await this.apiClient.conversation.api.postOTRMessage(
70
- sendingClientId,
71
- conversationId,
72
- message,
73
- ignoreMissing,
74
- );
88
+ return await send(encryptedPayload);
75
89
  } catch (error) {
76
90
  if (!this.isClientMismatchError(error)) {
77
91
  throw error;
78
92
  }
79
- const reEncryptedMessage = await this.onClientMismatch(
80
- error.response!.data,
81
- {...message, data: base64CipherText ? Decoder.fromBase64(base64CipherText).asBytes : undefined, recipients},
82
- plainTextArray,
83
- );
84
- return await this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, {
85
- data: reEncryptedMessage.data ? Encoder.toBase64(reEncryptedMessage.data).asString : undefined,
86
- recipients: CryptographyService.convertArrayRecipientsToBase64(reEncryptedMessage.recipients),
87
- sender: reEncryptedMessage.sender,
88
- });
93
+ const mismatch = error.response!.data as ClientMismatch;
94
+ const reEncryptedMessage = await this.reencryptAfterMismatch(mismatch, encryptedPayload, plainText);
95
+ return send(reEncryptedMessage);
89
96
  }
90
97
  }
91
98
 
92
- private isClientMismatchError(error: any): error is ClientMismatchError {
93
- return error.response?.status === HTTP_STATUS.PRECONDITION_FAILED;
94
- }
95
-
96
- private checkFederatedClientsMismatch(
97
- messageData: ProtobufOTR.QualifiedNewOtrMessage,
98
- messageSendingStatus: MessageSendingStatus,
99
- ): MessageSendingStatus | null {
100
- const updatedMessageSendingStatus = {...messageSendingStatus};
101
- const sendingStatusKeys: (keyof Omit<typeof updatedMessageSendingStatus, 'time'>)[] = [
102
- 'deleted',
103
- 'failed_to_send',
104
- 'missing',
105
- 'redundant',
106
- ];
107
-
108
- const allFailed: QualifiedUserClients = {
109
- ...messageSendingStatus.deleted,
110
- ...messageSendingStatus.failed_to_send,
111
- ...messageSendingStatus.missing,
112
- ...messageSendingStatus.redundant,
99
+ /**
100
+ * Sends a message to a federated backend.
101
+ *
102
+ * @param sendingClientId The clientId of the current user
103
+ * @param recipients The list of recipients to send the message to
104
+ * @param plainText The plainText data to send
105
+ * @param options.conversationId? the conversation to send the message to. Will broadcast if not set
106
+ * @param options.reportMissing? trigger a mismatch error when there are missing recipients in the payload
107
+ * @param options.sendAsProtobuf?
108
+ * @param options.onClientMismatch? Called when a mismatch happens on the server
109
+ * @return the MessageSendingStatus returned by the backend
110
+ */
111
+ public async sendFederatedMessage(
112
+ sendingClientId: string,
113
+ recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
114
+ plainText: Uint8Array,
115
+ options: {
116
+ assetData?: Uint8Array;
117
+ conversationId?: QualifiedId;
118
+ reportMissing?: boolean;
119
+ onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
120
+ },
121
+ ): Promise<MessageSendingStatus> {
122
+ const send = (payload: QualifiedOTRRecipients) => {
123
+ return this.sendFederatedOtrMessage(sendingClientId, payload, options);
113
124
  };
114
- const hasDiffs = Object.keys(allFailed).length;
115
- if (!hasDiffs) {
116
- return null;
117
- }
118
-
119
- if (messageData.ignoreOnly?.userIds?.length) {
120
- for (const [domainFailed, userClientsFailed] of Object.entries(allFailed)) {
121
- for (const userIdMissing of Object.keys(userClientsFailed)) {
122
- const userIsIgnored = messageData.ignoreOnly.userIds.find(({domain: domainIgnore, id: userIdIgnore}) => {
123
- return userIdIgnore === userIdMissing && domainIgnore === domainFailed;
124
- });
125
- if (userIsIgnored) {
126
- for (const sendingStatusKey of sendingStatusKeys) {
127
- delete updatedMessageSendingStatus[sendingStatusKey][domainFailed][userIdMissing];
128
- }
129
- }
130
- }
125
+ const encryptedPayload = await this.cryptographyService.encryptQualified(plainText, recipients);
126
+ try {
127
+ return await send(encryptedPayload);
128
+ } catch (error) {
129
+ if (!this.isClientMismatchError(error)) {
130
+ throw error;
131
131
  }
132
- } else if (messageData.reportOnly?.userIds?.length) {
133
- for (const [reportDomain, reportUserId] of Object.entries(messageData.reportOnly.userIds)) {
134
- for (const sendingStatusKey of sendingStatusKeys) {
135
- for (const [domainDeleted, userClientsDeleted] of Object.entries(
136
- updatedMessageSendingStatus[sendingStatusKey],
137
- )) {
138
- for (const userIdDeleted of Object.keys(userClientsDeleted)) {
139
- if (userIdDeleted !== reportUserId.id && domainDeleted !== reportDomain) {
140
- delete updatedMessageSendingStatus[sendingStatusKey][domainDeleted][userIdDeleted];
141
- }
142
- }
143
- }
144
- }
132
+ const mismatch = error.response!.data as MessageSendingStatus;
133
+ const shouldStopSending = options.onClientMismatch && !(await options.onClientMismatch(mismatch));
134
+ if (shouldStopSending) {
135
+ return mismatch;
145
136
  }
146
- } else if (!!messageData.ignoreAll) {
147
- // report nothing
148
- return null;
137
+ const reEncryptedPayload = await this.reencryptAfterFederatedMismatch(mismatch, encryptedPayload, plainText);
138
+ return send(reEncryptedPayload);
149
139
  }
150
-
151
- return updatedMessageSendingStatus;
152
140
  }
153
141
 
154
- public async sendFederatedOTRMessage(
142
+ private async sendFederatedOtrMessage(
155
143
  sendingClientId: string,
156
- {id: conversationId, domain}: QualifiedId,
157
- recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
158
- plainTextArray: Uint8Array,
144
+ recipients: QualifiedOTRRecipients,
159
145
  options: {
160
146
  assetData?: Uint8Array;
147
+ conversationId?: QualifiedId;
161
148
  reportMissing?: boolean;
162
149
  onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
163
- } = {},
150
+ },
164
151
  ): Promise<MessageSendingStatus> {
165
- const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
166
-
167
- const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
152
+ const qualifiedUserEntries = Object.entries(recipients).map<ProtobufOTR.IQualifiedUserEntry>(
168
153
  ([domain, otrRecipients]) => {
169
154
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
170
155
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -199,240 +184,96 @@ export class MessageService {
199
184
  protoMessage.blob = options.assetData;
200
185
  }
201
186
 
202
- /*
203
- * When creating the PreKey bundles we already found out to which users we want to send a message, so we can ignore
204
- * missing clients. We have to ignore missing clients because there can be the case that there are clients that
205
- * don't provide PreKeys (clients from the Pre-E2EE era).
206
- */
207
187
  if (options.reportMissing) {
208
188
  protoMessage.reportAll = {};
209
189
  } else {
210
190
  protoMessage.ignoreAll = {};
211
191
  }
212
192
 
213
- let sendingStatus: MessageSendingStatus;
214
- let sendingFailed: boolean = false;
215
- try {
216
- sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, protoMessage);
217
- } catch (error) {
218
- if (!this.isClientMismatchError(error)) {
219
- throw error;
220
- }
221
- sendingStatus = error.response!.data! as unknown as MessageSendingStatus;
222
- sendingFailed = true;
193
+ if (!options.conversationId) {
194
+ //TODO implement federated broadcast sending
195
+ throw new Error('Unimplemented federated broadcast');
223
196
  }
224
197
 
225
- const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
198
+ const {id, domain} = options.conversationId;
226
199
 
227
- if (mismatch) {
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);
234
- }
235
- return sendingStatus;
200
+ return this.apiClient.conversation.api.postOTRMessageV2(id, domain, protoMessage);
236
201
  }
237
202
 
238
- public async sendOTRProtobufMessage(
203
+ private async sendOTRMessage(
239
204
  sendingClientId: string,
240
205
  recipients: OTRRecipients<Uint8Array>,
241
- conversationId: string | null,
242
- plainTextArray: Uint8Array,
243
- assetData?: Uint8Array,
206
+ options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
244
207
  ): Promise<ClientMismatch> {
245
- const userEntries: ProtobufOTR.IUserEntry[] = Object.entries(recipients).map(([userId, otrClientMap]) => {
246
- const clients: ProtobufOTR.IClientEntry[] = Object.entries(otrClientMap).map(([clientId, payload]) => {
247
- return {
248
- client: {
249
- client: Long.fromString(clientId, 16),
250
- },
251
- text: payload,
252
- };
253
- });
254
-
255
- return {
256
- clients,
257
- user: {
258
- uuid: uuidToBytes(userId),
259
- },
260
- };
261
- });
262
-
263
- const protoMessage = ProtobufOTR.NewOtrMessage.create({
264
- recipients: userEntries,
265
- sender: {
266
- client: Long.fromString(sendingClientId, 16),
267
- },
268
- });
269
-
270
- if (assetData) {
271
- protoMessage.blob = assetData;
272
- }
273
-
274
- /*
275
- * When creating the PreKey bundles we already found out to which users we want to send a message, so we can ignore
276
- * missing clients. We have to ignore missing clients because there can be the case that there are clients that
277
- * don't provide PreKeys (clients from the Pre-E2EE era).
278
- */
279
- const ignoreMissing = true;
208
+ const message: NewOTRMessage<string> = {
209
+ data: options.assetData ? Encoder.toBase64(options.assetData).asString : undefined,
210
+ recipients: CryptographyService.convertArrayRecipientsToBase64(recipients),
211
+ sender: sendingClientId,
212
+ };
280
213
 
281
- try {
282
- if (conversationId === null) {
283
- return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(
214
+ return !options.conversationId
215
+ ? this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, message, !options.reportMissing)
216
+ : this.apiClient.conversation.api.postOTRMessage(
284
217
  sendingClientId,
285
- protoMessage,
286
- ignoreMissing,
218
+ options.conversationId,
219
+ message,
220
+ !options.reportMissing,
287
221
  );
288
- }
289
- return await this.apiClient.conversation.api.postOTRProtobufMessage(
290
- sendingClientId,
291
- conversationId,
292
- protoMessage,
293
- ignoreMissing,
294
- );
295
- } catch (error) {
296
- if (!this.isClientMismatchError(error)) {
297
- throw error;
298
- }
299
- const mismatch = error.response!.data;
300
- const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
301
- if (conversationId === null) {
302
- return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
303
- }
304
- return await this.apiClient.conversation.api.postOTRProtobufMessage(
305
- sendingClientId,
306
- conversationId,
307
- reEncryptedMessage,
308
- ignoreMissing,
309
- );
310
- }
311
222
  }
312
223
 
313
- private async onClientMismatch(
314
- clientMismatch: ClientMismatch,
315
- message: NewOTRMessage<Uint8Array>,
316
- plainTextArray: Uint8Array,
317
- ): Promise<NewOTRMessage<Uint8Array>> {
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];
329
- }
330
- }
331
- }
332
- }
224
+ private async generateExternalPayload(plainText: Uint8Array): Promise<{text: Uint8Array; cipherText: Uint8Array}> {
225
+ const asset = await encryptAsset({plainText});
226
+ const {cipherText, keyBytes, sha256} = asset;
227
+ const messageId = MessageBuilder.createId();
333
228
 
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] = {};
342
- }
343
-
344
- message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
345
- }
346
- }
347
- }
229
+ const externalMessage = {
230
+ otrKey: new Uint8Array(keyBytes),
231
+ sha256: new Uint8Array(sha256),
232
+ };
348
233
 
349
- return message;
234
+ const genericMessage = GenericMessage.create({
235
+ [GenericMessageType.EXTERNAL]: externalMessage,
236
+ messageId,
237
+ });
238
+
239
+ return {text: GenericMessage.encode(genericMessage).finish(), cipherText};
350
240
  }
351
241
 
352
- private async onClientProtobufMismatch(
353
- clientMismatch: {missing: UserClients; deleted: UserClients},
354
- message: ProtobufOTR.NewOtrMessage,
355
- plainTextArray: Uint8Array,
356
- ): Promise<ProtobufOTR.NewOtrMessage> {
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!];
372
- }
373
- }
374
- }
375
- }
376
- }
242
+ private shouldSendAsExternal(plainText: Uint8Array, preKeyBundles: UserPreKeyBundleMap | UserClients): boolean {
243
+ const EXTERNAL_MESSAGE_THRESHOLD_BYTES = 200 * 1024;
377
244
 
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),
390
- },
391
- text: reEncryptedPayloads[missingUserId][missingClientId],
392
- },
393
- ],
394
- user: {
395
- uuid: uuidToBytes(missingUserId),
396
- },
397
- });
398
- }
399
- }
400
- }
245
+ let clientCount = 0;
246
+ for (const user in preKeyBundles) {
247
+ clientCount += Object.keys(preKeyBundles[user]).length;
401
248
  }
402
249
 
403
- return message;
250
+ const messageInBytes = new Uint8Array(plainText).length;
251
+ const estimatedPayloadInBytes = clientCount * messageInBytes;
252
+
253
+ return estimatedPayloadInBytes > EXTERNAL_MESSAGE_THRESHOLD_BYTES;
404
254
  }
405
255
 
406
- private deleteExtraQualifiedClients(
407
- message: ProtobufOTR.QualifiedNewOtrMessage,
408
- deletedClients: MessageSendingStatus['deleted'],
409
- ): ProtobufOTR.QualifiedNewOtrMessage {
410
- // walk through deleted domain/user map
411
- for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
412
- if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
413
- // no user from this domain was deleted
414
- continue;
415
- }
416
- // walk through deleted user ids
417
- for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
418
- // walk through message recipients
419
- for (const recipients of message.recipients) {
420
- // check if message recipients' domain is the same as the deleted user's domain
421
- if (recipients.domain === deletedUserDomain) {
422
- // check if message recipients' id is the same as the deleted user's id
423
- for (const recipientEntry of recipients.entries || []) {
424
- const uuid = recipientEntry.user?.uuid;
425
- if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
426
- // delete this user from the message recipients
427
- const deleteIndex = recipients.entries!.indexOf(recipientEntry);
428
- recipients.entries!.splice(deleteIndex);
429
- }
430
- }
431
- }
432
- }
433
- }
256
+ private isClientMismatchError(error: any): error is ClientMismatchError {
257
+ return error.response?.status === HTTP_STATUS.PRECONDITION_FAILED;
258
+ }
259
+
260
+ private async reencryptAfterMismatch(
261
+ mismatch: ClientMismatch,
262
+ recipients: OTRRecipients<Uint8Array>,
263
+ plainText: Uint8Array,
264
+ ): Promise<OTRRecipients<Uint8Array>> {
265
+ const deleted = flattenUserClients(mismatch.deleted);
266
+ const missing = flattenUserClients(mismatch.missing);
267
+ // remove deleted clients to the recipients
268
+ deleted.forEach(({userId, data}) => data.forEach(clientId => delete recipients[userId.id][clientId]));
269
+ if (missing.length) {
270
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(mismatch.missing);
271
+ const reEncrypted = await this.cryptographyService.encrypt(plainText, missingPreKeyBundles);
272
+ const reEncryptedPayloads = flattenUserClients<{[client: string]: Uint8Array}>(reEncrypted);
273
+ // add missing clients to the recipients
274
+ reEncryptedPayloads.forEach(({data, userId}) => (recipients[userId.id] = {...recipients[userId.id], ...data}));
434
275
  }
435
- return message;
276
+ return recipients;
436
277
  }
437
278
 
438
279
  /**
@@ -443,63 +284,70 @@ export class MessageService {
443
284
  * @param {Uint8Array} plainText The text that should be encrypted for the missing clients
444
285
  * @return resolves with a new message payload that can be sent
445
286
  */
446
- private async onFederatedMismatch(
447
- message: ProtobufOTR.QualifiedNewOtrMessage,
448
- {deleted, missing}: MessageSendingStatus,
287
+ private async reencryptAfterFederatedMismatch(
288
+ mismatch: MessageSendingStatus,
289
+ recipients: QualifiedOTRRecipients,
449
290
  plainText: Uint8Array,
450
- ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
451
- message = this.deleteExtraQualifiedClients(message, deleted);
291
+ ): Promise<QualifiedOTRRecipients> {
292
+ const deleted = flattenQualifiedUserClients(mismatch.deleted);
293
+ const missing = flattenQualifiedUserClients(mismatch.missing);
294
+ // remove deleted clients to the recipients
295
+ deleted.forEach(({userId, data}) =>
296
+ data.forEach(clientId => delete recipients[userId.domain][userId.id][clientId]),
297
+ );
298
+
452
299
  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);
300
+ const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(mismatch.missing);
301
+ const reEncrypted = await this.cryptographyService.encryptQualified(plainText, missingPreKeyBundles);
302
+ const reEncryptedPayloads = flattenQualifiedUserClients<{[client: string]: Uint8Array}>(reEncrypted);
303
+ reEncryptedPayloads.forEach(
304
+ ({data, userId}) => (recipients[userId.domain][userId.id] = {...recipients[userId.domain][userId.id], ...data}),
305
+ );
456
306
  }
457
- return message;
307
+ return recipients;
458
308
  }
459
309
 
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
- }
484
-
485
- const missingUserUUID = domain.entries![userIndex!].user.uuid;
486
-
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
- });
496
- }
497
- }
498
- }
499
- }
500
- }
310
+ private async sendOTRProtobufMessage(
311
+ sendingClientId: string,
312
+ recipients: OTRRecipients<Uint8Array>,
313
+ options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
314
+ ): Promise<ClientMismatch> {
315
+ const userEntries: ProtobufOTR.IUserEntry[] = Object.entries(recipients).map(([userId, otrClientMap]) => {
316
+ const clients: ProtobufOTR.IClientEntry[] = Object.entries(otrClientMap).map(([clientId, payload]) => {
317
+ return {
318
+ client: {
319
+ client: Long.fromString(clientId, 16),
320
+ },
321
+ text: payload,
322
+ };
323
+ });
324
+
325
+ return {
326
+ clients,
327
+ user: {
328
+ uuid: uuidToBytes(userId),
329
+ },
330
+ };
331
+ });
332
+
333
+ const protoMessage = ProtobufOTR.NewOtrMessage.create({
334
+ recipients: userEntries,
335
+ sender: {
336
+ client: Long.fromString(sendingClientId, 16),
337
+ },
338
+ });
339
+
340
+ if (options.assetData) {
341
+ protoMessage.blob = options.assetData;
501
342
  }
502
343
 
503
- return messageData;
344
+ return !options.conversationId
345
+ ? this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, protoMessage, !options.reportMissing)
346
+ : this.apiClient.conversation.api.postOTRProtobufMessage(
347
+ sendingClientId,
348
+ options.conversationId,
349
+ protoMessage,
350
+ !options.reportMissing,
351
+ );
504
352
  }
505
353
  }