@wireapp/core 17.22.1 → 17.24.2

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 +46 -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 +4 -6
  11. package/src/main/conversation/ConversationService.js +26 -59
  12. package/src/main/conversation/ConversationService.js.map +1 -1
  13. package/src/main/conversation/ConversationService.test.node.js +0 -38
  14. package/src/main/conversation/ConversationService.test.node.js.map +1 -1
  15. package/src/main/conversation/ConversationService.test.node.ts +1 -50
  16. package/src/main/conversation/ConversationService.ts +32 -116
  17. package/src/main/conversation/message/MessageService.d.ts +42 -12
  18. package/src/main/conversation/message/MessageService.js +147 -275
  19. package/src/main/conversation/message/MessageService.js.map +1 -1
  20. package/src/main/conversation/message/MessageService.test.node.js +117 -10
  21. package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
  22. package/src/main/conversation/message/MessageService.test.node.ts +183 -40
  23. package/src/main/conversation/message/MessageService.ts +209 -354
  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,135 +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
- if (messageData.ignoreOnly?.userIds?.length) {
109
- const allFailed: QualifiedUserClients = {
110
- ...messageSendingStatus.deleted,
111
- ...messageSendingStatus.failed_to_send,
112
- ...messageSendingStatus.missing,
113
- ...messageSendingStatus.redundant,
114
- };
115
-
116
- for (const [domainFailed, userClientsFailed] of Object.entries(allFailed)) {
117
- for (const userIdMissing of Object.keys(userClientsFailed)) {
118
- const userIsIgnored = messageData.ignoreOnly.userIds.find(({domain: domainIgnore, id: userIdIgnore}) => {
119
- return userIdIgnore === userIdMissing && domainIgnore === domainFailed;
120
- });
121
- if (userIsIgnored) {
122
- for (const sendingStatusKey of sendingStatusKeys) {
123
- delete updatedMessageSendingStatus[sendingStatusKey][domainFailed][userIdMissing];
124
- }
125
- }
126
- }
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);
124
+ };
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;
127
131
  }
128
- } else if (messageData.reportOnly?.userIds?.length) {
129
- for (const [reportDomain, reportUserId] of Object.entries(messageData.reportOnly.userIds)) {
130
- for (const sendingStatusKey of sendingStatusKeys) {
131
- for (const [domainDeleted, userClientsDeleted] of Object.entries(
132
- updatedMessageSendingStatus[sendingStatusKey],
133
- )) {
134
- for (const userIdDeleted of Object.keys(userClientsDeleted)) {
135
- if (userIdDeleted !== reportUserId.id && domainDeleted !== reportDomain) {
136
- delete updatedMessageSendingStatus[sendingStatusKey][domainDeleted][userIdDeleted];
137
- }
138
- }
139
- }
140
- }
132
+ const mismatch = error.response!.data as MessageSendingStatus;
133
+ const shouldStopSending = options.onClientMismatch && !(await options.onClientMismatch(mismatch));
134
+ if (shouldStopSending) {
135
+ return mismatch;
141
136
  }
142
- } else if (!!messageData.ignoreAll) {
143
- // report nothing
144
- return null;
137
+ const reEncryptedPayload = await this.reencryptAfterFederatedMismatch(mismatch, encryptedPayload, plainText);
138
+ return send(reEncryptedPayload);
145
139
  }
146
-
147
- return updatedMessageSendingStatus;
148
140
  }
149
141
 
150
- public async sendFederatedOTRMessage(
142
+ private async sendFederatedOtrMessage(
151
143
  sendingClientId: string,
152
- {id: conversationId, domain}: QualifiedId,
153
- recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
154
- plainTextArray: Uint8Array,
144
+ recipients: QualifiedOTRRecipients,
155
145
  options: {
156
146
  assetData?: Uint8Array;
147
+ conversationId?: QualifiedId;
157
148
  reportMissing?: boolean;
158
149
  onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
159
- } = {},
150
+ },
160
151
  ): Promise<MessageSendingStatus> {
161
- const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
162
-
163
- const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
152
+ const qualifiedUserEntries = Object.entries(recipients).map<ProtobufOTR.IQualifiedUserEntry>(
164
153
  ([domain, otrRecipients]) => {
165
154
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
166
155
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -195,237 +184,96 @@ export class MessageService {
195
184
  protoMessage.blob = options.assetData;
196
185
  }
197
186
 
198
- /*
199
- * When creating the PreKey bundles we already found out to which users we want to send a message, so we can ignore
200
- * missing clients. We have to ignore missing clients because there can be the case that there are clients that
201
- * don't provide PreKeys (clients from the Pre-E2EE era).
202
- */
203
187
  if (options.reportMissing) {
204
188
  protoMessage.reportAll = {};
205
189
  } else {
206
190
  protoMessage.ignoreAll = {};
207
191
  }
208
192
 
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;
193
+ if (!options.conversationId) {
194
+ //TODO implement federated broadcast sending
195
+ throw new Error('Unimplemented federated broadcast');
217
196
  }
218
197
 
219
- const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
198
+ const {id, domain} = options.conversationId;
220
199
 
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);
228
- }
229
- return sendingStatus;
200
+ return this.apiClient.conversation.api.postOTRMessageV2(id, domain, protoMessage);
230
201
  }
231
202
 
232
- public async sendOTRProtobufMessage(
203
+ private async sendOTRMessage(
233
204
  sendingClientId: string,
234
205
  recipients: OTRRecipients<Uint8Array>,
235
- conversationId: string | null,
236
- plainTextArray: Uint8Array,
237
- assetData?: Uint8Array,
206
+ options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
238
207
  ): Promise<ClientMismatch> {
239
- const userEntries: ProtobufOTR.IUserEntry[] = Object.entries(recipients).map(([userId, otrClientMap]) => {
240
- const clients: ProtobufOTR.IClientEntry[] = Object.entries(otrClientMap).map(([clientId, payload]) => {
241
- return {
242
- client: {
243
- client: Long.fromString(clientId, 16),
244
- },
245
- text: payload,
246
- };
247
- });
248
-
249
- return {
250
- clients,
251
- user: {
252
- uuid: uuidToBytes(userId),
253
- },
254
- };
255
- });
256
-
257
- const protoMessage = ProtobufOTR.NewOtrMessage.create({
258
- recipients: userEntries,
259
- sender: {
260
- client: Long.fromString(sendingClientId, 16),
261
- },
262
- });
263
-
264
- if (assetData) {
265
- protoMessage.blob = assetData;
266
- }
267
-
268
- /*
269
- * When creating the PreKey bundles we already found out to which users we want to send a message, so we can ignore
270
- * missing clients. We have to ignore missing clients because there can be the case that there are clients that
271
- * don't provide PreKeys (clients from the Pre-E2EE era).
272
- */
273
- 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
+ };
274
213
 
275
- try {
276
- if (conversationId === null) {
277
- 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(
278
217
  sendingClientId,
279
- protoMessage,
280
- ignoreMissing,
218
+ options.conversationId,
219
+ message,
220
+ !options.reportMissing,
281
221
  );
282
- }
283
- return await this.apiClient.conversation.api.postOTRProtobufMessage(
284
- sendingClientId,
285
- conversationId,
286
- protoMessage,
287
- ignoreMissing,
288
- );
289
- } catch (error) {
290
- if (!this.isClientMismatchError(error)) {
291
- throw error;
292
- }
293
- const mismatch = error.response!.data;
294
- const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
295
- if (conversationId === null) {
296
- return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
297
- }
298
- return await this.apiClient.conversation.api.postOTRProtobufMessage(
299
- sendingClientId,
300
- conversationId,
301
- reEncryptedMessage,
302
- ignoreMissing,
303
- );
304
- }
305
222
  }
306
223
 
307
- private async onClientMismatch(
308
- clientMismatch: ClientMismatch,
309
- message: NewOTRMessage<Uint8Array>,
310
- plainTextArray: Uint8Array,
311
- ): Promise<NewOTRMessage<Uint8Array>> {
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];
323
- }
324
- }
325
- }
326
- }
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();
327
228
 
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] = {};
336
- }
337
-
338
- message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
339
- }
340
- }
341
- }
229
+ const externalMessage = {
230
+ otrKey: new Uint8Array(keyBytes),
231
+ sha256: new Uint8Array(sha256),
232
+ };
233
+
234
+ const genericMessage = GenericMessage.create({
235
+ [GenericMessageType.EXTERNAL]: externalMessage,
236
+ messageId,
237
+ });
342
238
 
343
- return message;
239
+ return {text: GenericMessage.encode(genericMessage).finish(), cipherText};
344
240
  }
345
241
 
346
- private async onClientProtobufMismatch(
347
- clientMismatch: {missing: UserClients; deleted: UserClients},
348
- message: ProtobufOTR.NewOtrMessage,
349
- plainTextArray: Uint8Array,
350
- ): Promise<ProtobufOTR.NewOtrMessage> {
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!];
366
- }
367
- }
368
- }
369
- }
370
- }
242
+ private shouldSendAsExternal(plainText: Uint8Array, preKeyBundles: UserPreKeyBundleMap | UserClients): boolean {
243
+ const EXTERNAL_MESSAGE_THRESHOLD_BYTES = 200 * 1024;
371
244
 
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),
384
- },
385
- text: reEncryptedPayloads[missingUserId][missingClientId],
386
- },
387
- ],
388
- user: {
389
- uuid: uuidToBytes(missingUserId),
390
- },
391
- });
392
- }
393
- }
394
- }
245
+ let clientCount = 0;
246
+ for (const user in preKeyBundles) {
247
+ clientCount += Object.keys(preKeyBundles[user]).length;
395
248
  }
396
249
 
397
- return message;
250
+ const messageInBytes = new Uint8Array(plainText).length;
251
+ const estimatedPayloadInBytes = clientCount * messageInBytes;
252
+
253
+ return estimatedPayloadInBytes > EXTERNAL_MESSAGE_THRESHOLD_BYTES;
398
254
  }
399
255
 
400
- private deleteExtraQualifiedClients(
401
- message: ProtobufOTR.QualifiedNewOtrMessage,
402
- deletedClients: MessageSendingStatus['deleted'],
403
- ): ProtobufOTR.QualifiedNewOtrMessage {
404
- // walk through deleted domain/user map
405
- for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
406
- if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
407
- // no user from this domain was deleted
408
- continue;
409
- }
410
- // walk through deleted user ids
411
- for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
412
- // walk through message recipients
413
- for (const recipientIndex in message.recipients) {
414
- // check if message recipients' domain is the same as the deleted user's domain
415
- if (message.recipients[recipientIndex].domain === deletedUserDomain) {
416
- // check if message recipients' id is the same as the deleted user's id
417
- for (const entriesIndex in message.recipients[recipientIndex].entries || []) {
418
- const uuid = message.recipients[recipientIndex].entries![entriesIndex].user?.uuid;
419
- if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
420
- // delete this user from the message recipients
421
- delete message.recipients[recipientIndex].entries![entriesIndex];
422
- }
423
- }
424
- }
425
- }
426
- }
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}));
427
275
  }
428
- return message;
276
+ return recipients;
429
277
  }
430
278
 
431
279
  /**
@@ -436,63 +284,70 @@ export class MessageService {
436
284
  * @param {Uint8Array} plainText The text that should be encrypted for the missing clients
437
285
  * @return resolves with a new message payload that can be sent
438
286
  */
439
- private async onFederatedMismatch(
440
- message: ProtobufOTR.QualifiedNewOtrMessage,
441
- {deleted, missing}: MessageSendingStatus,
287
+ private async reencryptAfterFederatedMismatch(
288
+ mismatch: MessageSendingStatus,
289
+ recipients: QualifiedOTRRecipients,
442
290
  plainText: Uint8Array,
443
- ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
444
- 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
+
445
299
  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);
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
+ );
449
306
  }
450
- return message;
307
+ return recipients;
451
308
  }
452
309
 
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
- }
477
-
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
- });
489
- }
490
- }
491
- }
492
- }
493
- }
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;
494
342
  }
495
343
 
496
- 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
+ );
497
352
  }
498
353
  }