@wireapp/core 17.24.0 → 17.26.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 (36) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +2 -2
  3. package/src/main/broadcast/BroadcastService.js +3 -4
  4. package/src/main/broadcast/BroadcastService.js.map +1 -1
  5. package/src/main/broadcast/BroadcastService.ts +3 -5
  6. package/src/main/conversation/ConversationService.d.ts +16 -9
  7. package/src/main/conversation/ConversationService.js +52 -65
  8. package/src/main/conversation/ConversationService.js.map +1 -1
  9. package/src/main/conversation/ConversationService.test.node.js +17 -38
  10. package/src/main/conversation/ConversationService.test.node.js.map +1 -1
  11. package/src/main/conversation/ConversationService.test.node.ts +20 -50
  12. package/src/main/conversation/ConversationService.ts +72 -124
  13. package/src/main/conversation/message/MessageService.d.ts +43 -13
  14. package/src/main/conversation/message/MessageService.js +150 -281
  15. package/src/main/conversation/message/MessageService.js.map +1 -1
  16. package/src/main/conversation/message/MessageService.test.node.js +178 -6
  17. package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
  18. package/src/main/conversation/message/MessageService.test.node.ts +245 -29
  19. package/src/main/conversation/message/MessageService.ts +211 -364
  20. package/src/main/conversation/message/UserClientsUtil.d.ts +22 -0
  21. package/src/main/conversation/message/UserClientsUtil.js +38 -0
  22. package/src/main/conversation/message/UserClientsUtil.js.map +1 -0
  23. package/src/main/conversation/message/UserClientsUtil.ts +44 -0
  24. package/src/main/conversation/message/UserClientsUtils.test.node.d.ts +1 -0
  25. package/src/main/conversation/message/UserClientsUtils.test.node.js +42 -0
  26. package/src/main/conversation/message/UserClientsUtils.test.node.js.map +1 -0
  27. package/src/main/conversation/message/UserClientsUtils.test.node.ts +44 -0
  28. package/src/main/cryptography/CryptographyService.js +8 -0
  29. package/src/main/cryptography/CryptographyService.js.map +1 -1
  30. package/src/main/cryptography/CryptographyService.test.node.js +2 -2
  31. package/src/main/cryptography/CryptographyService.test.node.js.map +1 -1
  32. package/src/main/cryptography/CryptographyService.test.node.ts +5 -4
  33. package/src/main/cryptography/CryptographyService.ts +10 -2
  34. package/src/main/util/TypePredicateUtil.js +3 -9
  35. package/src/main/util/TypePredicateUtil.js.map +1 -1
  36. 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,123 @@ 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) => undefined | 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
- });
89
- }
90
- }
91
-
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,
113
- };
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
- }
93
+ const mismatch = error.response!.data as ClientMismatch;
94
+ const shouldStopSending = options.onClientMismatch && (await options.onClientMismatch(mismatch)) === false;
95
+ if (shouldStopSending) {
96
+ return mismatch;
131
97
  }
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
- }
145
- }
146
- } else if (!!messageData.ignoreAll) {
147
- // report nothing
148
- return null;
98
+ const reEncryptedMessage = await this.reencryptAfterMismatch(mismatch, encryptedPayload, plainText);
99
+ return send(reEncryptedMessage);
149
100
  }
150
-
151
- return updatedMessageSendingStatus;
152
101
  }
153
102
 
154
- public async sendFederatedOTRMessage(
103
+ /**
104
+ * Sends a message to a federated backend.
105
+ *
106
+ * @param sendingClientId The clientId of the current user
107
+ * @param recipients The list of recipients to send the message to
108
+ * @param plainText The plainText data to send
109
+ * @param options.conversationId? the conversation to send the message to. Will broadcast if not set
110
+ * @param options.reportMissing? trigger a mismatch error when there are missing recipients in the payload
111
+ * @param options.sendAsProtobuf?
112
+ * @param options.onClientMismatch? Called when a mismatch happens on the server
113
+ * @return the MessageSendingStatus returned by the backend
114
+ */
115
+ public async sendFederatedMessage(
155
116
  sendingClientId: string,
156
- {id: conversationId, domain}: QualifiedId,
157
117
  recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
158
- plainTextArray: Uint8Array,
118
+ plainText: Uint8Array,
159
119
  options: {
160
120
  assetData?: Uint8Array;
121
+ conversationId?: QualifiedId;
161
122
  reportMissing?: boolean;
162
- onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
163
- } = {},
123
+ onClientMismatch?: (mismatch: MessageSendingStatus) => undefined | Promise<boolean | undefined>;
124
+ },
164
125
  ): Promise<MessageSendingStatus> {
165
- const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
126
+ const send = (payload: QualifiedOTRRecipients) => {
127
+ return this.sendFederatedOtrMessage(sendingClientId, payload, options);
128
+ };
129
+ const encryptedPayload = await this.cryptographyService.encryptQualified(plainText, recipients);
130
+ try {
131
+ return await send(encryptedPayload);
132
+ } catch (error) {
133
+ if (!this.isClientMismatchError(error)) {
134
+ throw error;
135
+ }
136
+ const mismatch = error.response!.data as MessageSendingStatus;
137
+ const shouldStopSending = options.onClientMismatch && (await options.onClientMismatch(mismatch)) === false;
138
+ if (shouldStopSending) {
139
+ return mismatch;
140
+ }
141
+ const reEncryptedPayload = await this.reencryptAfterFederatedMismatch(mismatch, encryptedPayload, plainText);
142
+ return send(reEncryptedPayload);
143
+ }
144
+ }
166
145
 
167
- const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
146
+ private async sendFederatedOtrMessage(
147
+ sendingClientId: string,
148
+ recipients: QualifiedOTRRecipients,
149
+ options: {assetData?: Uint8Array; conversationId?: QualifiedId; reportMissing?: boolean},
150
+ ): Promise<MessageSendingStatus> {
151
+ const qualifiedUserEntries = Object.entries(recipients).map<ProtobufOTR.IQualifiedUserEntry>(
168
152
  ([domain, otrRecipients]) => {
169
153
  const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
170
154
  const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
@@ -199,240 +183,96 @@ export class MessageService {
199
183
  protoMessage.blob = options.assetData;
200
184
  }
201
185
 
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
186
  if (options.reportMissing) {
208
187
  protoMessage.reportAll = {};
209
188
  } else {
210
189
  protoMessage.ignoreAll = {};
211
190
  }
212
191
 
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;
192
+ if (!options.conversationId) {
193
+ //TODO implement federated broadcast sending
194
+ throw new Error('Unimplemented federated broadcast');
223
195
  }
224
196
 
225
- const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
197
+ const {id, domain} = options.conversationId;
226
198
 
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;
199
+ return this.apiClient.conversation.api.postOTRMessageV2(id, domain, protoMessage);
236
200
  }
237
201
 
238
- public async sendOTRProtobufMessage(
202
+ private async sendOTRMessage(
239
203
  sendingClientId: string,
240
204
  recipients: OTRRecipients<Uint8Array>,
241
- conversationId: string | null,
242
- plainTextArray: Uint8Array,
243
- assetData?: Uint8Array,
205
+ options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
244
206
  ): 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;
207
+ const message: NewOTRMessage<string> = {
208
+ data: options.assetData ? Encoder.toBase64(options.assetData).asString : undefined,
209
+ recipients: CryptographyService.convertArrayRecipientsToBase64(recipients),
210
+ sender: sendingClientId,
211
+ };
280
212
 
281
- try {
282
- if (conversationId === null) {
283
- return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(
213
+ return !options.conversationId
214
+ ? this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, message, !options.reportMissing)
215
+ : this.apiClient.conversation.api.postOTRMessage(
284
216
  sendingClientId,
285
- protoMessage,
286
- ignoreMissing,
217
+ options.conversationId,
218
+ message,
219
+ !options.reportMissing,
287
220
  );
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
221
  }
312
222
 
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
- }
223
+ private async generateExternalPayload(plainText: Uint8Array): Promise<{text: Uint8Array; cipherText: Uint8Array}> {
224
+ const asset = await encryptAsset({plainText});
225
+ const {cipherText, keyBytes, sha256} = asset;
226
+ const messageId = MessageBuilder.createId();
333
227
 
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
- }
228
+ const externalMessage = {
229
+ otrKey: new Uint8Array(keyBytes),
230
+ sha256: new Uint8Array(sha256),
231
+ };
348
232
 
349
- return message;
233
+ const genericMessage = GenericMessage.create({
234
+ [GenericMessageType.EXTERNAL]: externalMessage,
235
+ messageId,
236
+ });
237
+
238
+ return {text: GenericMessage.encode(genericMessage).finish(), cipherText};
350
239
  }
351
240
 
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
- }
241
+ private shouldSendAsExternal(plainText: Uint8Array, preKeyBundles: UserPreKeyBundleMap | UserClients): boolean {
242
+ const EXTERNAL_MESSAGE_THRESHOLD_BYTES = 200 * 1024;
377
243
 
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
- }
244
+ let clientCount = 0;
245
+ for (const user in preKeyBundles) {
246
+ clientCount += Object.keys(preKeyBundles[user]).length;
401
247
  }
402
248
 
403
- return message;
249
+ const messageInBytes = new Uint8Array(plainText).length;
250
+ const estimatedPayloadInBytes = clientCount * messageInBytes;
251
+
252
+ return estimatedPayloadInBytes > EXTERNAL_MESSAGE_THRESHOLD_BYTES;
404
253
  }
405
254
 
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
- }
255
+ private isClientMismatchError(error: any): error is ClientMismatchError {
256
+ return error.response?.status === HTTP_STATUS.PRECONDITION_FAILED;
257
+ }
258
+
259
+ private async reencryptAfterMismatch(
260
+ mismatch: ClientMismatch,
261
+ recipients: OTRRecipients<Uint8Array>,
262
+ plainText: Uint8Array,
263
+ ): Promise<OTRRecipients<Uint8Array>> {
264
+ const deleted = flattenUserClients(mismatch.deleted);
265
+ const missing = flattenUserClients(mismatch.missing);
266
+ // remove deleted clients to the recipients
267
+ deleted.forEach(({userId, data}) => data.forEach(clientId => delete recipients[userId.id][clientId]));
268
+ if (missing.length) {
269
+ const missingPreKeyBundles = await this.apiClient.user.api.postMultiPreKeyBundles(mismatch.missing);
270
+ const reEncrypted = await this.cryptographyService.encrypt(plainText, missingPreKeyBundles);
271
+ const reEncryptedPayloads = flattenUserClients<{[client: string]: Uint8Array}>(reEncrypted);
272
+ // add missing clients to the recipients
273
+ reEncryptedPayloads.forEach(({data, userId}) => (recipients[userId.id] = {...recipients[userId.id], ...data}));
434
274
  }
435
- return message;
275
+ return recipients;
436
276
  }
437
277
 
438
278
  /**
@@ -443,63 +283,70 @@ export class MessageService {
443
283
  * @param {Uint8Array} plainText The text that should be encrypted for the missing clients
444
284
  * @return resolves with a new message payload that can be sent
445
285
  */
446
- private async onFederatedMismatch(
447
- message: ProtobufOTR.QualifiedNewOtrMessage,
448
- {deleted, missing}: MessageSendingStatus,
286
+ private async reencryptAfterFederatedMismatch(
287
+ mismatch: MessageSendingStatus,
288
+ recipients: QualifiedOTRRecipients,
449
289
  plainText: Uint8Array,
450
- ): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
451
- message = this.deleteExtraQualifiedClients(message, deleted);
290
+ ): Promise<QualifiedOTRRecipients> {
291
+ const deleted = flattenQualifiedUserClients(mismatch.deleted);
292
+ const missing = flattenQualifiedUserClients(mismatch.missing);
293
+ // remove deleted clients to the recipients
294
+ deleted.forEach(({userId, data}) =>
295
+ data.forEach(clientId => delete recipients[userId.domain][userId.id][clientId]),
296
+ );
297
+
452
298
  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);
299
+ const missingPreKeyBundles = await this.apiClient.user.api.postQualifiedMultiPreKeyBundles(mismatch.missing);
300
+ const reEncrypted = await this.cryptographyService.encryptQualified(plainText, missingPreKeyBundles);
301
+ const reEncryptedPayloads = flattenQualifiedUserClients<{[client: string]: Uint8Array}>(reEncrypted);
302
+ reEncryptedPayloads.forEach(
303
+ ({data, userId}) => (recipients[userId.domain][userId.id] = {...recipients[userId.domain][userId.id], ...data}),
304
+ );
456
305
  }
457
- return message;
306
+ return recipients;
458
307
  }
459
308
 
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
- }
309
+ private async sendOTRProtobufMessage(
310
+ sendingClientId: string,
311
+ recipients: OTRRecipients<Uint8Array>,
312
+ options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
313
+ ): Promise<ClientMismatch> {
314
+ const userEntries: ProtobufOTR.IUserEntry[] = Object.entries(recipients).map(([userId, otrClientMap]) => {
315
+ const clients: ProtobufOTR.IClientEntry[] = Object.entries(otrClientMap).map(([clientId, payload]) => {
316
+ return {
317
+ client: {
318
+ client: Long.fromString(clientId, 16),
319
+ },
320
+ text: payload,
321
+ };
322
+ });
323
+
324
+ return {
325
+ clients,
326
+ user: {
327
+ uuid: uuidToBytes(userId),
328
+ },
329
+ };
330
+ });
331
+
332
+ const protoMessage = ProtobufOTR.NewOtrMessage.create({
333
+ recipients: userEntries,
334
+ sender: {
335
+ client: Long.fromString(sendingClientId, 16),
336
+ },
337
+ });
338
+
339
+ if (options.assetData) {
340
+ protoMessage.blob = options.assetData;
501
341
  }
502
342
 
503
- return messageData;
343
+ return !options.conversationId
344
+ ? this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, protoMessage, !options.reportMissing)
345
+ : this.apiClient.conversation.api.postOTRProtobufMessage(
346
+ sendingClientId,
347
+ options.conversationId,
348
+ protoMessage,
349
+ !options.reportMissing,
350
+ );
504
351
  }
505
352
  }