@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.
- package/CHANGELOG.md +41 -0
- package/package.json +2 -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 +16 -9
- package/src/main/conversation/ConversationService.js +52 -65
- 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 +72 -124
- package/src/main/conversation/message/MessageService.d.ts +43 -13
- package/src/main/conversation/message/MessageService.js +150 -281
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.js +178 -6
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.ts +245 -29
- package/src/main/conversation/message/MessageService.ts +211 -364
- 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.js +8 -0
- 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 +10 -2
- 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,123 @@ 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) => undefined | 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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
192
|
+
if (!options.conversationId) {
|
|
193
|
+
//TODO implement federated broadcast sending
|
|
194
|
+
throw new Error('Unimplemented federated broadcast');
|
|
223
195
|
}
|
|
224
196
|
|
|
225
|
-
const
|
|
197
|
+
const {id, domain} = options.conversationId;
|
|
226
198
|
|
|
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;
|
|
199
|
+
return this.apiClient.conversation.api.postOTRMessageV2(id, domain, protoMessage);
|
|
236
200
|
}
|
|
237
201
|
|
|
238
|
-
|
|
202
|
+
private async sendOTRMessage(
|
|
239
203
|
sendingClientId: string,
|
|
240
204
|
recipients: OTRRecipients<Uint8Array>,
|
|
241
|
-
|
|
242
|
-
plainTextArray: Uint8Array,
|
|
243
|
-
assetData?: Uint8Array,
|
|
205
|
+
options: {conversationId?: string; assetData?: Uint8Array; reportMissing?: boolean},
|
|
244
206
|
): 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;
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
213
|
+
return !options.conversationId
|
|
214
|
+
? this.apiClient.broadcast.api.postBroadcastMessage(sendingClientId, message, !options.reportMissing)
|
|
215
|
+
: this.apiClient.conversation.api.postOTRMessage(
|
|
284
216
|
sendingClientId,
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
228
|
+
const externalMessage = {
|
|
229
|
+
otrKey: new Uint8Array(keyBytes),
|
|
230
|
+
sha256: new Uint8Array(sha256),
|
|
231
|
+
};
|
|
348
232
|
|
|
349
|
-
|
|
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
|
|
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
|
-
}
|
|
241
|
+
private shouldSendAsExternal(plainText: Uint8Array, preKeyBundles: UserPreKeyBundleMap | UserClients): boolean {
|
|
242
|
+
const EXTERNAL_MESSAGE_THRESHOLD_BYTES = 200 * 1024;
|
|
377
243
|
|
|
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
|
-
}
|
|
244
|
+
let clientCount = 0;
|
|
245
|
+
for (const user in preKeyBundles) {
|
|
246
|
+
clientCount += Object.keys(preKeyBundles[user]).length;
|
|
401
247
|
}
|
|
402
248
|
|
|
403
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
286
|
+
private async reencryptAfterFederatedMismatch(
|
|
287
|
+
mismatch: MessageSendingStatus,
|
|
288
|
+
recipients: QualifiedOTRRecipients,
|
|
449
289
|
plainText: Uint8Array,
|
|
450
|
-
): Promise<
|
|
451
|
-
|
|
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
|
|
455
|
-
|
|
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
|
|
306
|
+
return recipients;
|
|
458
307
|
}
|
|
459
308
|
|
|
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
|
-
}
|
|
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
|
|
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
|
}
|