@wireapp/core 17.21.1 → 17.24.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 +46 -0
- package/package.json +3 -3
- 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/conversation/ConversationService.d.ts +20 -6
- package/src/main/conversation/ConversationService.js +77 -42
- package/src/main/conversation/ConversationService.js.map +1 -1
- package/src/main/conversation/ConversationService.ts +91 -81
- package/src/main/conversation/message/MessageService.d.ts +11 -4
- package/src/main/conversation/message/MessageService.js +146 -130
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.js +74 -18
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.ts +118 -31
- package/src/main/conversation/message/MessageService.ts +173 -159
- package/src/main/cryptography/CryptographyService.d.ts +6 -1
- package/src/main/cryptography/CryptographyService.js +6 -2
- package/src/main/cryptography/CryptographyService.js.map +1 -1
- package/src/main/cryptography/CryptographyService.ts +12 -2
- package/src/main/util/TypePredicateUtil.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.js +6 -2
- package/src/main/util/TypePredicateUtil.js.map +1 -1
- package/src/main/util/TypePredicateUtil.ts +5 -1
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
import UUID from 'uuidjs';
|
|
21
21
|
import {StatusCodes} from 'http-status-codes';
|
|
22
22
|
import {APIClient} from '@wireapp/api-client';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
MessageSendingStatus,
|
|
25
|
+
OTRRecipients,
|
|
26
|
+
QualifiedOTRRecipients,
|
|
27
|
+
QualifiedUserClients,
|
|
28
|
+
} from '@wireapp/api-client/src/conversation';
|
|
24
29
|
import {CryptographyService} from '../../cryptography';
|
|
25
30
|
import {MessageService} from './MessageService';
|
|
26
31
|
|
|
@@ -36,60 +41,142 @@ type TestUser = {id: string; domain: string; clients: string[]};
|
|
|
36
41
|
const user1: TestUser = {id: UUID.genV4().toString(), domain: '1.wire.test', clients: ['client1.1', 'client1.2']};
|
|
37
42
|
const user2: TestUser = {id: UUID.genV4().toString(), domain: '2.wire.test', clients: ['client2.1', 'client2.2']};
|
|
38
43
|
|
|
39
|
-
function
|
|
40
|
-
const payload:
|
|
44
|
+
function generateQualifiedRecipients(users: TestUser[]): QualifiedUserClients {
|
|
45
|
+
const payload: QualifiedUserClients = {};
|
|
41
46
|
users.forEach(({id, domain, clients}) => {
|
|
42
47
|
payload[domain] ||= {};
|
|
43
|
-
payload[domain][id] =
|
|
44
|
-
clients.forEach(client => (payload[domain][id][client] = new Uint8Array()));
|
|
48
|
+
payload[domain][id] = clients;
|
|
45
49
|
});
|
|
46
50
|
return payload;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function fakeEncrypt(_: unknown, recipients: QualifiedUserClients): Promise<QualifiedOTRRecipients> {
|
|
54
|
+
const encryptedPayload = Object.entries(recipients).reduce((acc, [domain, users]) => {
|
|
55
|
+
acc[domain] = Object.entries(users).reduce((userClients, [userId, clients]) => {
|
|
56
|
+
userClients[userId] = clients.reduce((payloads, client) => {
|
|
57
|
+
payloads[client] = new Uint8Array();
|
|
58
|
+
return payloads;
|
|
59
|
+
}, {} as any);
|
|
60
|
+
return userClients;
|
|
61
|
+
}, {} as OTRRecipients<Uint8Array>);
|
|
62
|
+
return acc;
|
|
63
|
+
}, {} as QualifiedOTRRecipients);
|
|
64
|
+
return Promise.resolve(encryptedPayload);
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
describe('MessageService', () => {
|
|
50
68
|
const apiClient = new APIClient();
|
|
51
69
|
const cryptographyService = new CryptographyService(apiClient, {} as any);
|
|
52
70
|
const messageService = new MessageService(apiClient, cryptographyService);
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
spyOn(cryptographyService, 'encryptQualified').and.callFake(fakeEncrypt);
|
|
74
|
+
});
|
|
75
|
+
|
|
53
76
|
describe('sendFederatedMessage', () => {
|
|
54
77
|
it('sends a message', async () => {
|
|
55
78
|
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.returnValue(Promise.resolve(baseMessageSendingStatus));
|
|
56
|
-
const recipients
|
|
79
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
57
80
|
|
|
58
|
-
await messageService.sendFederatedOTRMessage(
|
|
81
|
+
await messageService.sendFederatedOTRMessage(
|
|
82
|
+
'senderclientid',
|
|
83
|
+
{id: 'convid', domain: ''},
|
|
84
|
+
recipients,
|
|
85
|
+
new Uint8Array(),
|
|
86
|
+
);
|
|
59
87
|
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalled();
|
|
60
88
|
});
|
|
61
89
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
describe('client mismatch', () => {
|
|
91
|
+
it('handles client mismatch internally if no onClientMismatch is given', async () => {
|
|
92
|
+
let spyCounter = 0;
|
|
93
|
+
const clientMismatch = {
|
|
94
|
+
...baseMessageSendingStatus,
|
|
95
|
+
deleted: {[user1.domain]: {[user1.id]: [user1.clients[0]]}},
|
|
96
|
+
missing: {'2.wire.test': {[user2.id]: ['client22']}},
|
|
97
|
+
};
|
|
98
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
99
|
+
spyCounter++;
|
|
100
|
+
if (spyCounter === 1) {
|
|
101
|
+
const error = new Error();
|
|
102
|
+
(error as any).response = {
|
|
103
|
+
status: StatusCodes.PRECONDITION_FAILED,
|
|
104
|
+
data: clientMismatch,
|
|
105
|
+
};
|
|
106
|
+
return Promise.reject(error);
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve(baseMessageSendingStatus);
|
|
109
|
+
});
|
|
110
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
111
|
+
|
|
112
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
113
|
+
|
|
114
|
+
await messageService.sendFederatedOTRMessage(
|
|
115
|
+
'senderclientid',
|
|
116
|
+
{id: 'convid', domain: ''},
|
|
117
|
+
recipients,
|
|
118
|
+
new Uint8Array(),
|
|
119
|
+
{reportMissing: true},
|
|
120
|
+
);
|
|
121
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('continues message sending if onClientMismatch returns true', async () => {
|
|
125
|
+
const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(true));
|
|
126
|
+
const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
|
|
127
|
+
let spyCounter = 0;
|
|
128
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
129
|
+
spyCounter++;
|
|
130
|
+
if (spyCounter === 1) {
|
|
131
|
+
const error = new Error();
|
|
132
|
+
(error as any).response = {
|
|
133
|
+
status: StatusCodes.PRECONDITION_FAILED,
|
|
134
|
+
data: clientMismatch,
|
|
135
|
+
};
|
|
136
|
+
return Promise.reject(error);
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve(baseMessageSendingStatus);
|
|
139
|
+
});
|
|
140
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
141
|
+
|
|
142
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
143
|
+
|
|
144
|
+
await messageService.sendFederatedOTRMessage(
|
|
145
|
+
'senderclientid',
|
|
146
|
+
{id: 'convid', domain: ''},
|
|
147
|
+
recipients,
|
|
148
|
+
new Uint8Array(),
|
|
149
|
+
{reportMissing: true, onClientMismatch},
|
|
150
|
+
);
|
|
151
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('stops message sending if onClientMismatch returns false', async () => {
|
|
156
|
+
const onClientMismatch = jasmine.createSpy().and.returnValue(Promise.resolve(false));
|
|
157
|
+
const clientMismatch = {...baseMessageSendingStatus, missing: {'2.wire.test': {[user2.id]: ['client22']}}};
|
|
158
|
+
spyOn(apiClient.conversation.api, 'postOTRMessageV2').and.callFake(() => {
|
|
67
159
|
const error = new Error();
|
|
68
160
|
(error as any).response = {
|
|
69
161
|
status: StatusCodes.PRECONDITION_FAILED,
|
|
70
|
-
data:
|
|
162
|
+
data: clientMismatch,
|
|
71
163
|
};
|
|
72
164
|
return Promise.reject(error);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
77
|
-
spyOn(cryptographyService, 'encryptQualified').and.returnValue(
|
|
78
|
-
Promise.resolve({'2.wire.test': {[user2.id]: {client22: new Uint8Array()}}}),
|
|
79
|
-
);
|
|
165
|
+
});
|
|
166
|
+
spyOn(apiClient.user.api, 'postQualifiedMultiPreKeyBundles').and.returnValue(Promise.resolve({}));
|
|
80
167
|
|
|
81
|
-
|
|
168
|
+
const recipients = generateQualifiedRecipients([user1, user2]);
|
|
82
169
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
170
|
+
await messageService.sendFederatedOTRMessage(
|
|
171
|
+
'senderclientid',
|
|
172
|
+
{id: 'convid', domain: ''},
|
|
173
|
+
recipients,
|
|
174
|
+
new Uint8Array(),
|
|
175
|
+
{reportMissing: true, onClientMismatch},
|
|
176
|
+
);
|
|
177
|
+
expect(apiClient.conversation.api.postOTRMessageV2).toHaveBeenCalledTimes(1);
|
|
178
|
+
expect(onClientMismatch).toHaveBeenCalledWith(clientMismatch);
|
|
179
|
+
});
|
|
93
180
|
});
|
|
94
181
|
});
|
|
95
182
|
});
|
|
@@ -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
|
);
|
|
@@ -104,14 +105,18 @@ export class MessageService {
|
|
|
104
105
|
'redundant',
|
|
105
106
|
];
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
118
|
|
|
119
|
+
if (messageData.ignoreOnly?.userIds?.length) {
|
|
115
120
|
for (const [domainFailed, userClientsFailed] of Object.entries(allFailed)) {
|
|
116
121
|
for (const userIdMissing of Object.keys(userClientsFailed)) {
|
|
117
122
|
const userIsIgnored = messageData.ignoreOnly.userIds.find(({domain: domainIgnore, id: userIdIgnore}) => {
|
|
@@ -148,14 +153,18 @@ export class MessageService {
|
|
|
148
153
|
|
|
149
154
|
public async sendFederatedOTRMessage(
|
|
150
155
|
sendingClientId: string,
|
|
151
|
-
conversationId:
|
|
152
|
-
|
|
153
|
-
recipients: QualifiedOTRRecipients,
|
|
156
|
+
{id: conversationId, domain}: QualifiedId,
|
|
157
|
+
recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap,
|
|
154
158
|
plainTextArray: Uint8Array,
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
options: {
|
|
160
|
+
assetData?: Uint8Array;
|
|
161
|
+
reportMissing?: boolean;
|
|
162
|
+
onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
|
|
163
|
+
} = {},
|
|
157
164
|
): Promise<MessageSendingStatus> {
|
|
158
|
-
const
|
|
165
|
+
const otrRecipients = await this.cryptographyService.encryptQualified(plainTextArray, recipients);
|
|
166
|
+
|
|
167
|
+
const qualifiedUserEntries = Object.entries(otrRecipients).map<ProtobufOTR.IQualifiedUserEntry>(
|
|
159
168
|
([domain, otrRecipients]) => {
|
|
160
169
|
const userEntries = Object.entries(otrRecipients).map<ProtobufOTR.IUserEntry>(([userId, otrClientMap]) => {
|
|
161
170
|
const clientEntries = Object.entries(otrClientMap).map<ProtobufOTR.IClientEntry>(([clientId, payload]) => {
|
|
@@ -186,8 +195,8 @@ export class MessageService {
|
|
|
186
195
|
},
|
|
187
196
|
});
|
|
188
197
|
|
|
189
|
-
if (assetData) {
|
|
190
|
-
protoMessage.blob = assetData;
|
|
198
|
+
if (options.assetData) {
|
|
199
|
+
protoMessage.blob = options.assetData;
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
/*
|
|
@@ -195,31 +204,33 @@ export class MessageService {
|
|
|
195
204
|
* missing clients. We have to ignore missing clients because there can be the case that there are clients that
|
|
196
205
|
* don't provide PreKeys (clients from the Pre-E2EE era).
|
|
197
206
|
*/
|
|
198
|
-
if (reportMissing) {
|
|
207
|
+
if (options.reportMissing) {
|
|
199
208
|
protoMessage.reportAll = {};
|
|
200
209
|
} else {
|
|
201
210
|
protoMessage.ignoreAll = {};
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
let sendingStatus: MessageSendingStatus;
|
|
214
|
+
let sendingFailed: boolean = false;
|
|
205
215
|
try {
|
|
206
|
-
sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(
|
|
207
|
-
conversationId,
|
|
208
|
-
conversationDomain,
|
|
209
|
-
protoMessage,
|
|
210
|
-
);
|
|
216
|
+
sendingStatus = await this.apiClient.conversation.api.postOTRMessageV2(conversationId, domain, protoMessage);
|
|
211
217
|
} catch (error) {
|
|
212
218
|
if (!this.isClientMismatchError(error)) {
|
|
213
219
|
throw error;
|
|
214
220
|
}
|
|
215
221
|
sendingStatus = error.response!.data! as unknown as MessageSendingStatus;
|
|
222
|
+
sendingFailed = true;
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
const mismatch = this.checkFederatedClientsMismatch(protoMessage, sendingStatus);
|
|
219
226
|
|
|
220
227
|
if (mismatch) {
|
|
221
|
-
const
|
|
222
|
-
|
|
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);
|
|
223
234
|
}
|
|
224
235
|
return sendingStatus;
|
|
225
236
|
}
|
|
@@ -282,7 +293,11 @@ export class MessageService {
|
|
|
282
293
|
ignoreMissing,
|
|
283
294
|
);
|
|
284
295
|
} catch (error) {
|
|
285
|
-
|
|
296
|
+
if (!this.isClientMismatchError(error)) {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
const mismatch = error.response!.data;
|
|
300
|
+
const reEncryptedMessage = await this.onClientProtobufMismatch(mismatch, protoMessage, plainTextArray);
|
|
286
301
|
if (conversationId === null) {
|
|
287
302
|
return await this.apiClient.broadcast.api.postBroadcastProtobufMessage(sendingClientId, reEncryptedMessage);
|
|
288
303
|
}
|
|
@@ -296,189 +311,188 @@ export class MessageService {
|
|
|
296
311
|
}
|
|
297
312
|
|
|
298
313
|
private async onClientMismatch(
|
|
299
|
-
|
|
314
|
+
clientMismatch: ClientMismatch,
|
|
300
315
|
message: NewOTRMessage<Uint8Array>,
|
|
301
316
|
plainTextArray: Uint8Array,
|
|
302
317
|
): Promise<NewOTRMessage<Uint8Array>> {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
for (const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
delete deletedUser[deletedClientId];
|
|
315
|
-
}
|
|
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];
|
|
316
329
|
}
|
|
317
330
|
}
|
|
318
331
|
}
|
|
332
|
+
}
|
|
319
333
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
|
|
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] = {};
|
|
331
342
|
}
|
|
343
|
+
|
|
344
|
+
message.recipients[missingUserId][missingClientId] = reEncryptedPayloads[missingUserId][missingClientId];
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
|
-
|
|
335
|
-
return message;
|
|
336
347
|
}
|
|
337
348
|
|
|
338
|
-
|
|
349
|
+
return message;
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
private async onClientProtobufMismatch(
|
|
342
|
-
|
|
353
|
+
clientMismatch: {missing: UserClients; deleted: UserClients},
|
|
343
354
|
message: ProtobufOTR.NewOtrMessage,
|
|
344
355
|
plainTextArray: Uint8Array,
|
|
345
356
|
): Promise<ProtobufOTR.NewOtrMessage> {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
delete message.recipients[deletedUserIndex].clients?.[deletedClientIndex!];
|
|
362
|
-
}
|
|
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!];
|
|
363
372
|
}
|
|
364
373
|
}
|
|
365
374
|
}
|
|
366
375
|
}
|
|
376
|
+
}
|
|
367
377
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
},
|
|
381
|
-
text: reEncryptedPayloads[missingUserId][missingClientId],
|
|
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),
|
|
382
390
|
},
|
|
383
|
-
|
|
384
|
-
user: {
|
|
385
|
-
uuid: uuidToBytes(missingUserId),
|
|
391
|
+
text: reEncryptedPayloads[missingUserId][missingClientId],
|
|
386
392
|
},
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
],
|
|
394
|
+
user: {
|
|
395
|
+
uuid: uuidToBytes(missingUserId),
|
|
396
|
+
},
|
|
397
|
+
});
|
|
389
398
|
}
|
|
390
399
|
}
|
|
391
400
|
}
|
|
392
|
-
|
|
393
|
-
return message;
|
|
394
401
|
}
|
|
395
402
|
|
|
396
|
-
|
|
403
|
+
return message;
|
|
397
404
|
}
|
|
398
405
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
* @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
|
|
404
|
-
* @param {Uint8Array} plainTextArray The text that should be encrypted for the missing clients
|
|
405
|
-
* @return resolves with a new message payload that can be sent
|
|
406
|
-
*/
|
|
407
|
-
private async encryptForMissingClients(
|
|
408
|
-
messageData: ProtobufOTR.QualifiedNewOtrMessage,
|
|
409
|
-
messageSendingStatus: MessageSendingStatus,
|
|
410
|
-
plainTextArray: Uint8Array,
|
|
411
|
-
): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
|
|
406
|
+
private deleteExtraQualifiedClients(
|
|
407
|
+
message: ProtobufOTR.QualifiedNewOtrMessage,
|
|
408
|
+
deletedClients: MessageSendingStatus['deleted'],
|
|
409
|
+
): ProtobufOTR.QualifiedNewOtrMessage {
|
|
412
410
|
// walk through deleted domain/user map
|
|
413
|
-
for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(
|
|
414
|
-
if (!
|
|
411
|
+
for (const [deletedUserDomain, deletedUserIdClients] of Object.entries(deletedClients)) {
|
|
412
|
+
if (!message.recipients.find(recipient => recipient.domain === deletedUserDomain)) {
|
|
415
413
|
// no user from this domain was deleted
|
|
416
414
|
continue;
|
|
417
415
|
}
|
|
418
416
|
// walk through deleted user ids
|
|
419
417
|
for (const [deletedUserId] of Object.entries(deletedUserIdClients)) {
|
|
420
418
|
// walk through message recipients
|
|
421
|
-
for (const
|
|
419
|
+
for (const recipients of message.recipients) {
|
|
422
420
|
// check if message recipients' domain is the same as the deleted user's domain
|
|
423
|
-
if (
|
|
421
|
+
if (recipients.domain === deletedUserDomain) {
|
|
424
422
|
// check if message recipients' id is the same as the deleted user's id
|
|
425
|
-
for (const
|
|
426
|
-
const uuid =
|
|
423
|
+
for (const recipientEntry of recipients.entries || []) {
|
|
424
|
+
const uuid = recipientEntry.user?.uuid;
|
|
427
425
|
if (!!uuid && bytesToUUID(uuid) === deletedUserId) {
|
|
428
426
|
// delete this user from the message recipients
|
|
429
|
-
|
|
427
|
+
const deleteIndex = recipients.entries!.indexOf(recipientEntry);
|
|
428
|
+
recipients.entries!.splice(deleteIndex);
|
|
430
429
|
}
|
|
431
430
|
}
|
|
432
431
|
}
|
|
433
432
|
}
|
|
434
433
|
}
|
|
435
434
|
}
|
|
435
|
+
return message;
|
|
436
|
+
}
|
|
436
437
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
438
|
+
/**
|
|
439
|
+
* 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)
|
|
440
|
+
*
|
|
441
|
+
* @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
|
|
442
|
+
* @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
|
|
443
|
+
* @param {Uint8Array} plainText The text that should be encrypted for the missing clients
|
|
444
|
+
* @return resolves with a new message payload that can be sent
|
|
445
|
+
*/
|
|
446
|
+
private async onFederatedMismatch(
|
|
447
|
+
message: ProtobufOTR.QualifiedNewOtrMessage,
|
|
448
|
+
{deleted, missing}: MessageSendingStatus,
|
|
449
|
+
plainText: Uint8Array,
|
|
450
|
+
): Promise<ProtobufOTR.QualifiedNewOtrMessage> {
|
|
451
|
+
message = this.deleteExtraQualifiedClients(message, deleted);
|
|
452
|
+
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);
|
|
456
|
+
}
|
|
457
|
+
return message;
|
|
458
|
+
}
|
|
443
459
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
484
|
|
|
451
|
-
|
|
452
|
-
for (const [missingUserId, missingClientIds] of Object.entries(missingUserIdClients)) {
|
|
453
|
-
// walk through message recipients
|
|
454
|
-
for (const domain of messageData.recipients) {
|
|
455
|
-
// check if message recipients' domain is the same as the missing user's domain
|
|
456
|
-
if (domain.domain === missingUserDomain) {
|
|
457
|
-
// check if there is a recipient with same user id as the missing user's id
|
|
458
|
-
let userIndex = domain.entries?.findIndex(({user}) => bytesToUUID(user.uuid) === missingUserId);
|
|
459
|
-
|
|
460
|
-
if (userIndex === -1) {
|
|
461
|
-
// no recipient found, let's create it
|
|
462
|
-
userIndex =
|
|
463
|
-
domain.entries!.push({
|
|
464
|
-
user: {
|
|
465
|
-
uuid: uuidToBytes(missingUserId),
|
|
466
|
-
},
|
|
467
|
-
}) - 1;
|
|
468
|
-
}
|
|
485
|
+
const missingUserUUID = domain.entries![userIndex!].user.uuid;
|
|
469
486
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
text: reEncryptedPayloads[missingUserDomain][missingUserId][missingClientId],
|
|
480
|
-
});
|
|
481
|
-
}
|
|
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
|
+
});
|
|
482
496
|
}
|
|
483
497
|
}
|
|
484
498
|
}
|