@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.
- package/CHANGELOG.md +41 -0
- package/package.json +2 -2
- package/src/main/Account.d.ts +10 -1
- package/src/main/Account.js +8 -2
- package/src/main/Account.js.map +1 -1
- package/src/main/Account.ts +15 -2
- package/src/main/broadcast/BroadcastService.js +3 -4
- package/src/main/broadcast/BroadcastService.js.map +1 -1
- package/src/main/broadcast/BroadcastService.ts +3 -5
- package/src/main/conversation/ConversationService.d.ts +7 -8
- package/src/main/conversation/ConversationService.js +32 -61
- package/src/main/conversation/ConversationService.js.map +1 -1
- package/src/main/conversation/ConversationService.test.node.js +17 -38
- package/src/main/conversation/ConversationService.test.node.js.map +1 -1
- package/src/main/conversation/ConversationService.test.node.ts +20 -50
- package/src/main/conversation/ConversationService.ts +40 -119
- package/src/main/conversation/message/MessageService.d.ts +42 -12
- package/src/main/conversation/message/MessageService.js +146 -281
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.js +100 -6
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.ts +153 -29
- package/src/main/conversation/message/MessageService.ts +208 -360
- package/src/main/conversation/message/UserClientsUtil.d.ts +22 -0
- package/src/main/conversation/message/UserClientsUtil.js +38 -0
- package/src/main/conversation/message/UserClientsUtil.js.map +1 -0
- package/src/main/conversation/message/UserClientsUtil.ts +44 -0
- package/src/main/conversation/message/UserClientsUtils.test.node.d.ts +1 -0
- package/src/main/conversation/message/UserClientsUtils.test.node.js +42 -0
- package/src/main/conversation/message/UserClientsUtils.test.node.js.map +1 -0
- package/src/main/conversation/message/UserClientsUtils.test.node.ts +44 -0
- package/src/main/cryptography/CryptographyService.d.ts +6 -1
- package/src/main/cryptography/CryptographyService.js +14 -2
- package/src/main/cryptography/CryptographyService.js.map +1 -1
- package/src/main/cryptography/CryptographyService.test.node.js +2 -2
- package/src/main/cryptography/CryptographyService.test.node.js.map +1 -1
- package/src/main/cryptography/CryptographyService.test.node.ts +5 -4
- package/src/main/cryptography/CryptographyService.ts +22 -4
- package/src/main/util/TypePredicateUtil.js +3 -9
- package/src/main/util/TypePredicateUtil.js.map +1 -1
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
142
|
+
private async sendFederatedOtrMessage(
|
|
155
143
|
sendingClientId: string,
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
198
|
+
const {id, domain} = options.conversationId;
|
|
226
199
|
|
|
227
|
-
|
|
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
|
-
|
|
203
|
+
private async sendOTRMessage(
|
|
239
204
|
sendingClientId: string,
|
|
240
205
|
recipients: OTRRecipients<Uint8Array>,
|
|
241
|
-
|
|
242
|
-
plainTextArray: Uint8Array,
|
|
243
|
-
assetData?: Uint8Array,
|
|
206
|
+
options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
|
|
244
207
|
): Promise<ClientMismatch> {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
214
|
+
return !options.conversationId
|
|
215
|
+
? this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, message, !options.reportMissing)
|
|
216
|
+
: this.apiClient.conversation.api.postOTRMessage(
|
|
284
217
|
sendingClientId,
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
|
353
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
287
|
+
private async reencryptAfterFederatedMismatch(
|
|
288
|
+
mismatch: MessageSendingStatus,
|
|
289
|
+
recipients: QualifiedOTRRecipients,
|
|
449
290
|
plainText: Uint8Array,
|
|
450
|
-
): Promise<
|
|
451
|
-
|
|
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
|
|
455
|
-
|
|
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
|
|
307
|
+
return recipients;
|
|
458
308
|
}
|
|
459
309
|
|
|
460
|
-
private
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
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
|
}
|