@wireapp/core 17.20.2 → 17.22.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 +3 -3
- package/src/main/conversation/ConversationService.d.ts +39 -5
- package/src/main/conversation/ConversationService.js +95 -38
- package/src/main/conversation/ConversationService.js.map +1 -1
- package/src/main/conversation/ConversationService.ts +110 -76
- package/src/main/conversation/message/MessageService.d.ts +19 -3
- package/src/main/conversation/message/MessageService.js +155 -119
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.d.ts +1 -0
- package/src/main/conversation/message/MessageService.test.node.js +123 -0
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -0
- package/src/main/conversation/message/MessageService.test.node.ts +163 -0
- package/src/main/conversation/message/MessageService.ts +176 -149
- package/src/main/cryptography/CryptographyService.d.ts +3 -3
- package/src/main/cryptography/CryptographyService.js +14 -11
- package/src/main/cryptography/CryptographyService.js.map +1 -1
- package/src/main/cryptography/CryptographyService.ts +23 -16
- package/src/main/util/TypePredicateUtil.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.js +7 -3
- package/src/main/util/TypePredicateUtil.js.map +1 -1
- package/src/main/util/TypePredicateUtil.test.node.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.test.node.js +42 -0
- package/src/main/util/TypePredicateUtil.test.node.js.map +1 -0
- package/src/main/util/TypePredicateUtil.test.node.ts +44 -0
- package/src/main/util/TypePredicateUtil.ts +6 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Wire
|
|
3
|
+
* Copyright (C) 2021 Wire Swiss GmbH
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU General Public License
|
|
16
|
+
* along with this program. If not, see http://www.gnu.org/licenses/.
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import UUID from 'uuidjs';
|
|
21
|
+
import {StatusCodes} from 'http-status-codes';
|
|
22
|
+
import {APIClient} from '@wireapp/api-client';
|
|
23
|
+
import {MessageSendingStatus, QualifiedUserClients} from '@wireapp/api-client/src/conversation';
|
|
24
|
+
import {CryptographyService} from '../../cryptography';
|
|
25
|
+
import {MessageService} from './MessageService';
|
|
26
|
+
|
|
27
|
+
const baseMessageSendingStatus: MessageSendingStatus = {
|
|
28
|
+
deleted: {},
|
|
29
|
+
missing: {},
|
|
30
|
+
failed_to_send: {},
|
|
31
|
+
redundant: {},
|
|
32
|
+
time: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type TestUser = {id: string; domain: string; clients: string[]};
|
|
36
|
+
const user1: TestUser = {id: UUID.genV4().toString(), domain: '1.wire.test', clients: ['client1.1', 'client1.2']};
|
|
37
|
+
const user2: TestUser = {id: UUID.genV4().toString(), domain: '2.wire.test', clients: ['client2.1', 'client2.2']};
|
|
38
|
+
|
|
39
|
+
function generateQualifiedRecipients(users: TestUser[]): QualifiedUserClients {
|
|
40
|
+
const payload: QualifiedUserClients = {};
|
|
41
|
+
users.forEach(({id, domain, clients}) => {
|
|
42
|
+
payload[domain] ||= {};
|
|
43
|
+
payload[domain][id] = clients;
|
|
44
|
+
});
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('MessageService', () => {
|
|
49
|
+
const apiClient = new APIClient();
|
|
50
|
+
const cryptographyService = new CryptographyService(apiClient, {} as any);
|
|
51
|
+
const messageService = new MessageService(apiClient, cryptographyService);
|
|
52
|
+
describe('sendFederatedMessage', () => {
|
|
53
|
+
it('sends a message', async () => {
|
|
54
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.returnValue(Promise.resolve(baseMessageSendingStatus));
|
|
55
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
56
|
+
|
|
57
|
+
await messageService.sendFederatedOTRMessage(
|
|
58
|
+
'senderclientid',
|
|
59
|
+
{id: 'convid', domain: ''},
|
|
60
|
+
recipients,
|
|
61
|
+
new Uint8Array(),
|
|
62
|
+
);
|
|
63
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('client mismatch', () => {
|
|
67
|
+
it('handles client mismatch internally if no onClientMismatch is given', async () => {
|
|
68
|
+
let spyCounter = 0;
|
|
69
|
+
const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
|
|
70
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
71
|
+
spyCounter++;
|
|
72
|
+
if (spyCounter === 1) {
|
|
73
|
+
const error = new Error();
|
|
74
|
+
(error as any).response = {
|
|
75
|
+
status: StatusCodes.PRECONDITION_FAILED,
|
|
76
|
+
data: clientMismatch,
|
|
77
|
+
};
|
|
78
|
+
return Promise.reject(error);
|
|
79
|
+
}
|
|
80
|
+
return Promise.resolve(baseMessageSendingStatus);
|
|
81
|
+
});
|
|
82
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
83
|
+
spyOn(cryptographyService, 'encryptQualified').and.returnValue(
|
|
84
|
+
Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
88
|
+
|
|
89
|
+
await messageService.sendFederatedOTRMessage(
|
|
90
|
+
'senderclientid',
|
|
91
|
+
{id: 'convid', domain: ''},
|
|
92
|
+
recipients,
|
|
93
|
+
new Uint8Array(),
|
|
94
|
+
{reportMissing: true},
|
|
95
|
+
);
|
|
96
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('continues message sending if onClientMismatch returns true', async () => {
|
|
100
|
+
const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(true));
|
|
101
|
+
const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
|
|
102
|
+
let spyCounter = 0;
|
|
103
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
104
|
+
spyCounter++;
|
|
105
|
+
if (spyCounter === 1) {
|
|
106
|
+
const error = new Error();
|
|
107
|
+
(error as any).response = {
|
|
108
|
+
status: StatusCodes.PRECONDITION_FAILED,
|
|
109
|
+
data: clientMismatch,
|
|
110
|
+
};
|
|
111
|
+
return Promise.reject(error);
|
|
112
|
+
}
|
|
113
|
+
return Promise.resolve(baseMessageSendingStatus);
|
|
114
|
+
});
|
|
115
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
116
|
+
spyOn(cryptographyService, 'encryptQualified').and.returnValue(
|
|
117
|
+
Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
121
|
+
|
|
122
|
+
await messageService.sendFederatedOTRMessage(
|
|
123
|
+
'senderclientid',
|
|
124
|
+
{id: 'convid', domain: ''},
|
|
125
|
+
recipients,
|
|
126
|
+
new Uint8Array(),
|
|
127
|
+
{reportMissing: true, onClientMismatch},
|
|
128
|
+
);
|
|
129
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
|
|
130
|
+
expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('stops message sending if onClientMismatch returns false', async () => {
|
|
134
|
+
const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(false));
|
|
135
|
+
const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
|
|
136
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
137
|
+
const error = new Error();
|
|
138
|
+
(error as any).response = {
|
|
139
|
+
status: StatusCodes.PRECONDITION_FAILED,
|
|
140
|
+
data: clientMismatch,
|
|
141
|
+
};
|
|
142
|
+
return Promise.reject(error);
|
|
143
|
+
});
|
|
144
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
145
|
+
spyOn(cryptographyService, 'encryptQualified').and.returnValue(
|
|
146
|
+
Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
150
|
+
|
|
151
|
+
await messageService.sendFederatedOTRMessage(
|
|
152
|
+
'senderclientid',
|
|
153
|
+
{id: 'convid', domain: ''},
|
|
154
|
+
recipients,
|
|
155
|
+
new Uint8Array(),
|
|
156
|
+
{reportMissing: true, onClientMismatch},
|
|
157
|
+
);
|
|
158
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -35,11 +35,9 @@ import {
|
|
|
35
35
|
import {Decoder, Encoder} from 'bazinga64';
|
|
36
36
|
|
|
37
37
|
import {CryptographyService} from '../../cryptography';
|
|
38
|
+
import {QualifiedId, QualifiedUserPreKeyBundleMap} from '@wireapp/api-client/src/user';
|
|
38
39
|
|
|
39
|
-
type ClientMismatchError = AxiosError<
|
|
40
|
-
deleted: UserClients;
|
|
41
|
-
missing: UserClients;
|
|
42
|
-
}>;
|
|
40
|
+
type ClientMismatchError = AxiosError<ClientMismatch>;
|
|
43
41
|
|
|
44
42
|
export class MessageService {
|
|
45
43
|
constructor(private readonly apiClient: APIClient, private readonly cryptographyService: CryptographyService) {}
|
|
@@ -75,8 +73,11 @@ export class MessageService {
|
|
|
75
73
|
ignoreMissing,
|
|
76
74
|
);
|
|
77
75
|
} catch (error) {
|
|
76
|
+
if (!this.isClientMismatchError(error)) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
78
79
|
const reEncryptedMessage = await this.onClientMismatch(
|
|
79
|
-
error
|
|
80
|
+
error.response!.data,
|
|
80
81
|
{...message, data: base64CipherText ? Decoder.fromBase64(base64CipherText).asBytes : undefined, recipients},
|
|
81
82
|
plainTextArray,
|
|
82
83
|
);
|
|
@@ -88,6 +89,10 @@ export class MessageService {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
private isClientMismatchError(error: any): error is ClientMismatchError {
|
|
93
|
+
return error.response?.status === HTTP_STATUS.PRECONDITION_FAILED;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
private checkFederatedClientsMismatch(
|
|
92
97
|
messageData: ProtobufOTR.QualifiedNewOtrMessage,
|
|
93
98
|
messageSendingStatus: MessageSendingStatus,
|
|
@@ -144,13 +149,18 @@ export class MessageService {
|
|
|
144
149
|
|
|
145
150
|
public async sendFederatedOTRMessage(
|
|
146
151
|
sendingClientId: string,
|
|
147
|
-
conversationId:
|
|
148
|
-
|
|
149
|
-
recipients: QualifiedOTRRecipients,
|
|
152
|
+
{id: conversationId, domain}: QualifiedId,
|
|
153
|
+
recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
|
|
150
154
|
plainTextArray: Uint8Array,
|
|
151
|
-
|
|
155
|
+
options: {
|
|
156
|
+
assetData?: Uint8Array;
|
|
157
|
+
reportMissing?: boolean;
|
|
158
|
+
onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
|
|
159
|
+
} = {},
|
|
152
160
|
): Promise<MessageSendingStatus> {
|
|
153
|
-
const
|
|
161
|
+
const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
|
|
162
|
+
|
|
163
|
+
const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
|
|
154
164
|
([domain, otrRecipients]) => {
|
|
155
165
|
const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
|
|
156
166
|
const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
|
|
@@ -181,8 +191,8 @@ export class MessageService {
|
|
|
181
191
|
},
|
|
182
192
|
});
|
|
183
193
|
|
|
184
|
-
if (assetData) {
|
|
185
|
-
protoMessage.blob = assetData;
|
|
194
|
+
if (options.assetData) {
|
|
195
|
+
protoMessage.blob = options.assetData;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
/*
|
|
@@ -190,25 +200,33 @@ export class MessageService {
|
|
|
190
200
|
* missing clients. We have to ignore missing clients because there can be the case that there are clients that
|
|
191
201
|
* don't provide PreKeys (clients from the Pre-E2EE era).
|
|
192
202
|
*/
|
|
193
|
-
|
|
203
|
+
if (options.reportMissing) {
|
|
204
|
+
protoMessage.reportAll = {};
|
|
205
|
+
} else {
|
|
206
|
+
protoMessage.ignoreAll = {};
|
|
207
|
+
}
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
217
|
+
}
|
|
200
218
|
|
|
201
|
-
const
|
|
219
|
+
const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
|
|
202
220
|
|
|
203
|
-
if (
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
await this.apiClient.conversation.api.postOTRMessageV2(conversationId,
|
|
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);
|
|
210
228
|
}
|
|
211
|
-
return
|
|
229
|
+
return sendingStatus;
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
public async sendOTRProtobufMessage(
|
|
@@ -269,7 +287,11 @@ export class MessageService {
|
|
|
269
287
|
ignoreMissing,
|
|
270
288
|
);
|
|
271
289
|
} catch (error) {
|
|
272
|
-
|
|
290
|
+
if (!this.isClientMismatchError(error)) {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
const mismatch = error.response!.data;
|
|
294
|
+
const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
|
|
273
295
|
if (conversationId === null) {
|
|
274
296
|
return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
|
|
275
297
|
}
|
|
@@ -283,182 +305,187 @@ export class MessageService {
|
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
private async onClientMismatch(
|
|
286
|
-
|
|
308
|
+
clientMismatch: ClientMismatch,
|
|
287
309
|
message: NewOTRMessage<Uint8Array>,
|
|
288
310
|
plainTextArray: Uint8Array,
|
|
289
311
|
): Promise<NewOTRMessage<Uint8Array>> {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for (const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
delete deletedUser[deletedClientId];
|
|
302
|
-
}
|
|
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];
|
|
303
323
|
}
|
|
304
324
|
}
|
|
305
325
|
}
|
|
326
|
+
}
|
|
306
327
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
|
|
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] = {};
|
|
318
336
|
}
|
|
337
|
+
|
|
338
|
+
message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
|
|
319
339
|
}
|
|
320
340
|
}
|
|
321
|
-
|
|
322
|
-
return message;
|
|
323
341
|
}
|
|
324
342
|
|
|
325
|
-
|
|
343
|
+
return message;
|
|
326
344
|
}
|
|
327
345
|
|
|
328
346
|
private async onClientProtobufMismatch(
|
|
329
|
-
|
|
347
|
+
clientMismatch: {missing: UserClients; deleted: UserClients},
|
|
330
348
|
message: ProtobufOTR.NewOtrMessage,
|
|
331
349
|
plainTextArray: Uint8Array,
|
|
332
350
|
): Promise<ProtobufOTR.NewOtrMessage> {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
for (const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
|
|
349
|
-
}
|
|
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!];
|
|
350
366
|
}
|
|
351
367
|
}
|
|
352
368
|
}
|
|
353
369
|
}
|
|
370
|
+
}
|
|
354
371
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
},
|
|
368
|
-
text: reEncryptedPayloads[missingUserId][missingClientId],
|
|
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),
|
|
369
384
|
},
|
|
370
|
-
|
|
371
|
-
user: {
|
|
372
|
-
uuid: uuidToBytes(missingUserId),
|
|
385
|
+
text: reEncryptedPayloads[missingUserId][missingClientId],
|
|
373
386
|
},
|
|
374
|
-
|
|
375
|
-
|
|
387
|
+
],
|
|
388
|
+
user: {
|
|
389
|
+
uuid: uuidToBytes(missingUserId),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
376
392
|
}
|
|
377
393
|
}
|
|
378
394
|
}
|
|
379
|
-
|
|
380
|
-
return message;
|
|
381
395
|
}
|
|
382
396
|
|
|
383
|
-
|
|
397
|
+
return message;
|
|
384
398
|
}
|
|
385
399
|
|
|
386
|
-
private
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
|
|
400
|
+
private deleteExtraQualifiedClients(
|
|
401
|
+
message: ProtobufOTR.QualifiedNewOtrMessage,
|
|
402
|
+
deletedClients: MessageSendingStatus['deleted'],
|
|
403
|
+
): ProtobufOTR.QualifiedNewOtrMessage {
|
|
391
404
|
// walk through deleted domain/user map
|
|
392
|
-
for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(
|
|
393
|
-
if (!
|
|
405
|
+
for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
|
|
406
|
+
if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
|
|
394
407
|
// no user from this domain was deleted
|
|
395
408
|
continue;
|
|
396
409
|
}
|
|
397
410
|
// walk through deleted user ids
|
|
398
411
|
for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
|
|
399
412
|
// walk through message recipients
|
|
400
|
-
for (const recipientIndex in
|
|
413
|
+
for (const recipientIndex in message.recipients) {
|
|
401
414
|
// check if message recipients' domain is the same as the deleted user's domain
|
|
402
|
-
if (
|
|
415
|
+
if (message.recipients[recipientIndex].domain === deletedUserDomain) {
|
|
403
416
|
// check if message recipients' id is the same as the deleted user's id
|
|
404
|
-
for (const entriesIndex in
|
|
405
|
-
const uuid =
|
|
417
|
+
for (const entriesIndex in message.recipients[recipientIndex].entries || []) {
|
|
418
|
+
const uuid = message.recipients[recipientIndex].entries![entriesIndex].user?.uuid;
|
|
406
419
|
if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
|
|
407
420
|
// delete this user from the message recipients
|
|
408
|
-
delete
|
|
421
|
+
delete message.recipients[recipientIndex].entries![entriesIndex];
|
|
409
422
|
}
|
|
410
423
|
}
|
|
411
424
|
}
|
|
412
425
|
}
|
|
413
426
|
}
|
|
414
427
|
}
|
|
428
|
+
return message;
|
|
429
|
+
}
|
|
415
430
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Will re-encrypt a message when there were some missing clients in the initial send (typically when the server replies with a client mismatch error)
|
|
433
|
+
*
|
|
434
|
+
* @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
|
|
435
|
+
* @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
|
|
436
|
+
* @param {Uint8Array} plainText The text that should be encrypted for the missing clients
|
|
437
|
+
* @return resolves with a new message payload that can be sent
|
|
438
|
+
*/
|
|
439
|
+
private async onFederatedMismatch(
|
|
440
|
+
message: ProtobufOTR.QualifiedNewOtrMessage,
|
|
441
|
+
{deleted, missing}: MessageSendingStatus,
|
|
442
|
+
plainText: Uint8Array,
|
|
443
|
+
): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
|
|
444
|
+
message = this.deleteExtraQualifiedClients(message, deleted);
|
|
445
|
+
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);
|
|
449
|
+
}
|
|
450
|
+
return message;
|
|
451
|
+
}
|
|
429
452
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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({
|
|
444
472
|
user: {
|
|
445
473
|
uuid: uuidToBytes(missingUserId),
|
|
446
474
|
},
|
|
447
|
-
});
|
|
448
|
-
|
|
475
|
+
}) - 1;
|
|
476
|
+
}
|
|
449
477
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
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
|
+
});
|
|
462
489
|
}
|
|
463
490
|
}
|
|
464
491
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { APIClient } from '@wireapp/api-client';
|
|
2
2
|
import type { PreKey as SerializedPreKey } from '@wireapp/api-client/src/auth/';
|
|
3
3
|
import type { RegisteredClient } from '@wireapp/api-client/src/client/';
|
|
4
|
-
import type { OTRRecipients, QualifiedOTRRecipients } from '@wireapp/api-client/src/conversation/';
|
|
4
|
+
import type { OTRRecipients, QualifiedOTRRecipients, QualifiedUserClients, UserClients } from '@wireapp/api-client/src/conversation/';
|
|
5
5
|
import type { ConversationOtrMessageAddEvent } from '@wireapp/api-client/src/event';
|
|
6
6
|
import type { QualifiedUserPreKeyBundleMap, UserPreKeyBundleMap } from '@wireapp/api-client/src/user/';
|
|
7
7
|
import { Cryptobox } from '@wireapp/cryptobox';
|
|
@@ -26,8 +26,8 @@ export declare class CryptographyService {
|
|
|
26
26
|
createCryptobox(): Promise<SerializedPreKey[]>;
|
|
27
27
|
decrypt(sessionId: string, encodedCiphertext: string): Promise<Uint8Array>;
|
|
28
28
|
private static dismantleSessionId;
|
|
29
|
-
encryptQualified(plainText: Uint8Array, preKeyBundles: QualifiedUserPreKeyBundleMap): Promise<QualifiedOTRRecipients>;
|
|
30
|
-
encrypt(plainText: Uint8Array,
|
|
29
|
+
encryptQualified(plainText: Uint8Array, preKeyBundles: QualifiedUserPreKeyBundleMap | QualifiedUserClients): Promise<QualifiedOTRRecipients>;
|
|
30
|
+
encrypt(plainText: Uint8Array, users: UserPreKeyBundleMap | UserClients, domain?: string): Promise<OTRRecipients<Uint8Array>>;
|
|
31
31
|
private encryptPayloadForSession;
|
|
32
32
|
initCryptobox(): Promise<void>;
|
|
33
33
|
deleteCryptographyStores(): Promise<boolean[]>;
|