]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
Implementing sending group messages with sender keys
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / SendHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.SignalDependencies;
4 import org.asamk.signal.manager.groups.GroupId;
5 import org.asamk.signal.manager.groups.GroupNotFoundException;
6 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
7 import org.asamk.signal.manager.groups.GroupUtils;
8 import org.asamk.signal.manager.groups.NotAGroupMemberException;
9 import org.asamk.signal.manager.storage.SignalAccount;
10 import org.asamk.signal.manager.storage.groups.GroupInfo;
11 import org.asamk.signal.manager.storage.recipients.Profile;
12 import org.asamk.signal.manager.storage.recipients.RecipientId;
13 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.whispersystems.libsignal.InvalidKeyException;
17 import org.whispersystems.libsignal.InvalidRegistrationIdException;
18 import org.whispersystems.libsignal.NoSessionException;
19 import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
20 import org.whispersystems.libsignal.util.guava.Optional;
21 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
22 import org.whispersystems.signalservice.api.crypto.ContentHint;
23 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
24 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
25 import org.whispersystems.signalservice.api.messages.SendMessageResult;
26 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
27 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
28 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
29 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
30 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
31 import org.whispersystems.signalservice.api.push.DistributionId;
32 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
33 import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
34 import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
35 import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
36 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
37 import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
38
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.concurrent.TimeUnit;
46 import java.util.stream.Collectors;
47
48 public class SendHelper {
49
50 private final static Logger logger = LoggerFactory.getLogger(SendHelper.class);
51
52 private final SignalAccount account;
53 private final SignalDependencies dependencies;
54 private final UnidentifiedAccessHelper unidentifiedAccessHelper;
55 private final SignalServiceAddressResolver addressResolver;
56 private final RecipientResolver recipientResolver;
57 private final IdentityFailureHandler identityFailureHandler;
58 private final GroupProvider groupProvider;
59 private final ProfileProvider profileProvider;
60 private final RecipientRegistrationRefresher recipientRegistrationRefresher;
61
62 public SendHelper(
63 final SignalAccount account,
64 final SignalDependencies dependencies,
65 final UnidentifiedAccessHelper unidentifiedAccessHelper,
66 final SignalServiceAddressResolver addressResolver,
67 final RecipientResolver recipientResolver,
68 final IdentityFailureHandler identityFailureHandler,
69 final GroupProvider groupProvider,
70 final ProfileProvider profileProvider,
71 final RecipientRegistrationRefresher recipientRegistrationRefresher
72 ) {
73 this.account = account;
74 this.dependencies = dependencies;
75 this.unidentifiedAccessHelper = unidentifiedAccessHelper;
76 this.addressResolver = addressResolver;
77 this.recipientResolver = recipientResolver;
78 this.identityFailureHandler = identityFailureHandler;
79 this.groupProvider = groupProvider;
80 this.profileProvider = profileProvider;
81 this.recipientRegistrationRefresher = recipientRegistrationRefresher;
82 }
83
84 /**
85 * Send a single message to one recipient.
86 * The message is extended with the current expiration timer.
87 */
88 public SendMessageResult sendMessage(
89 final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId
90 ) throws IOException {
91 final var contact = account.getContactStore().getContact(recipientId);
92 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
93 messageBuilder.withExpiration(expirationTime);
94 messageBuilder.withProfileKey(account.getProfileKey().serialize());
95
96 final var message = messageBuilder.build();
97 final var result = sendMessage(message, recipientId);
98 handleSendMessageResult(result);
99 return result;
100 }
101
102 /**
103 * Send a group message to the given group
104 * The message is extended with the current expiration timer for the group and the group context.
105 */
106 public List<SendMessageResult> sendAsGroupMessage(
107 SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
108 ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
109 final var g = getGroupForSending(groupId);
110 return sendAsGroupMessage(messageBuilder, g);
111 }
112
113 private List<SendMessageResult> sendAsGroupMessage(
114 final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
115 ) throws IOException, GroupSendingNotAllowedException {
116 GroupUtils.setGroupContext(messageBuilder, g);
117 messageBuilder.withExpiration(g.getMessageExpirationTimer());
118
119 final var message = messageBuilder.build();
120 final var recipients = g.getMembersWithout(account.getSelfRecipientId());
121
122 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
123 if (message.getBody().isPresent()
124 || message.getAttachments().isPresent()
125 || message.getQuote().isPresent()
126 || message.getPreviews().isPresent()
127 || message.getMentions().isPresent()
128 || message.getSticker().isPresent()) {
129 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
130 }
131 }
132
133 return sendGroupMessage(message, recipients, g.getDistributionId());
134 }
135
136 /**
137 * Send a complete group message to the given recipients (should be current/old/new members)
138 * This method should only be used for create/update/quit group messages.
139 */
140 public List<SendMessageResult> sendGroupMessage(
141 final SignalServiceDataMessage message,
142 final Set<RecipientId> recipientIds,
143 final DistributionId distributionId
144 ) throws IOException {
145 List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds, distributionId);
146
147 for (var r : result) {
148 handleSendMessageResult(r);
149 }
150
151 return result;
152 }
153
154 public SendMessageResult sendDeliveryReceipt(
155 RecipientId recipientId, List<Long> messageIds
156 ) {
157 var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
158 messageIds,
159 System.currentTimeMillis());
160
161 return sendReceiptMessage(receiptMessage, recipientId);
162 }
163
164 public SendMessageResult sendReceiptMessage(
165 final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
166 ) {
167 return handleSendMessage(recipientId,
168 (messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
169 unidentifiedAccess,
170 receiptMessage));
171 }
172
173 public SendMessageResult sendRetryReceipt(
174 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
175 ) {
176 logger.debug("Sending retry receipt for {} to {}, device: {}",
177 errorMessage.getTimestamp(),
178 recipientId,
179 errorMessage.getDeviceId());
180 return handleSendMessage(recipientId,
181 (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
182 unidentifiedAccess,
183 groupId.transform(GroupId::serialize),
184 errorMessage));
185 }
186
187 public SendMessageResult sendNullMessage(RecipientId recipientId) {
188 return handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
189 }
190
191 public SendMessageResult sendSelfMessage(
192 SignalServiceDataMessage.Builder messageBuilder
193 ) {
194 final var recipientId = account.getSelfRecipientId();
195 final var contact = account.getContactStore().getContact(recipientId);
196 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
197 messageBuilder.withExpiration(expirationTime);
198
199 var message = messageBuilder.build();
200 return sendSelfMessage(message);
201 }
202
203 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
204 var messageSender = dependencies.getMessageSender();
205 try {
206 return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync());
207 } catch (UnregisteredUserException e) {
208 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
209 return SendMessageResult.unregisteredFailure(address);
210 } catch (ProofRequiredException e) {
211 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
212 return SendMessageResult.proofRequiredFailure(address, e);
213 } catch (RateLimitException e) {
214 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
215 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
216 return SendMessageResult.networkFailure(address);
217 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
218 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
219 return SendMessageResult.identityFailure(address, e.getIdentityKey());
220 } catch (IOException e) {
221 var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId());
222 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
223 return SendMessageResult.networkFailure(address);
224 }
225 }
226
227 public SendMessageResult sendTypingMessage(
228 SignalServiceTypingMessage message, RecipientId recipientId
229 ) {
230 return handleSendMessage(recipientId,
231 (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(address,
232 unidentifiedAccess,
233 message));
234 }
235
236 public List<SendMessageResult> sendGroupTypingMessage(
237 SignalServiceTypingMessage message, GroupId groupId
238 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
239 final var g = getGroupForSending(groupId);
240 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
241 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
242 }
243 final var messageSender = dependencies.getMessageSender();
244 final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId()));
245 final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList();
246 return messageSender.sendTyping(addresses,
247 unidentifiedAccessHelper.getAccessFor(recipientIdList),
248 message,
249 null);
250 }
251
252 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
253 var g = groupProvider.getGroup(groupId);
254 if (g == null) {
255 throw new GroupNotFoundException(groupId);
256 }
257 if (!g.isMember(account.getSelfRecipientId())) {
258 throw new NotAGroupMemberException(groupId, g.getTitle());
259 }
260 return g;
261 }
262
263 private List<SendMessageResult> sendGroupMessageInternal(
264 final SignalServiceDataMessage message,
265 final Set<RecipientId> recipientIds,
266 final DistributionId distributionId
267 ) throws IOException {
268 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
269 final var isRecipientUpdate = false;
270 Set<RecipientId> senderKeyTargets = distributionId == null
271 ? Set.of()
272 : getSenderKeyCapableRecipientIds(recipientIds);
273 final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
274
275 if (senderKeyTargets.size() > 0) {
276 final var results = sendGroupMessageInternalWithSenderKey(message,
277 senderKeyTargets,
278 distributionId,
279 isRecipientUpdate);
280
281 if (results == null) {
282 senderKeyTargets = Set.of();
283 } else {
284 results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
285 final var failedTargets = results.stream()
286 .filter(r -> !r.isSuccess())
287 .map(r -> recipientResolver.resolveRecipient(r.getAddress()))
288 .toList();
289 if (failedTargets.size() > 0) {
290 senderKeyTargets = new HashSet<>(senderKeyTargets);
291 failedTargets.forEach(senderKeyTargets::remove);
292 }
293 }
294 }
295
296 final var legacyTargets = new HashSet<>(recipientIds);
297 legacyTargets.removeAll(senderKeyTargets);
298 final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
299
300 if (legacyTargets.size() > 0 || onlyTargetIsSelfWithLinkedDevice) {
301 if (legacyTargets.size() > 0) {
302 logger.debug("Need to do {} legacy sends.", legacyTargets.size());
303 } else {
304 logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
305 }
306
307 final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(message,
308 legacyTargets,
309 isRecipientUpdate || allResults.size() > 0);
310 allResults.addAll(results);
311 }
312
313 return allResults;
314 }
315
316 private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
317 final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId());
318 if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
319 logger.debug("Not all of our devices support sender key. Using legacy.");
320 return Set.of();
321 }
322
323 final var senderKeyTargets = new HashSet<RecipientId>();
324 for (final var recipientId : recipientIds) {
325 // TODO filter out unregistered
326 final var profile = profileProvider.getProfile(recipientId);
327 if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
328 continue;
329 }
330
331 final var access = unidentifiedAccessHelper.getAccessFor(recipientId);
332 if (!access.isPresent() || !access.get().getTargetUnidentifiedAccess().isPresent()) {
333 continue;
334 }
335
336 final var identity = account.getIdentityKeyStore().getIdentity(recipientId);
337 if (identity == null || !identity.getTrustLevel().isTrusted()) {
338 continue;
339 }
340
341 senderKeyTargets.add(recipientId);
342 }
343
344 if (senderKeyTargets.size() < 2) {
345 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
346 return Set.of();
347 }
348
349 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
350 return senderKeyTargets;
351 }
352
353 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
354 final SignalServiceDataMessage message, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
355 ) throws IOException {
356 final var recipientIdList = new ArrayList<>(recipientIds);
357 final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList();
358 final var unidentifiedAccesses = unidentifiedAccessHelper.getAccessFor(recipientIdList);
359 final var messageSender = dependencies.getMessageSender();
360 try {
361 final var results = messageSender.sendDataMessage(addresses,
362 unidentifiedAccesses,
363 isRecipientUpdate,
364 ContentHint.DEFAULT,
365 message,
366 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
367 sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
368 () -> false);
369
370 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
371 logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
372 return results;
373 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
374 return List.of();
375 }
376 }
377
378 private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
379 final SignalServiceDataMessage message,
380 final Set<RecipientId> recipientIds,
381 final DistributionId distributionId,
382 final boolean isRecipientUpdate
383 ) throws IOException {
384 final var recipientIdList = new ArrayList<>(recipientIds);
385 final var messageSender = dependencies.getMessageSender();
386
387 long keyCreateTime = account.getSenderKeyStore()
388 .getCreateTimeForOurKey(account.getSelfRecipientId(), account.getDeviceId(), distributionId);
389 long keyAge = System.currentTimeMillis() - keyCreateTime;
390
391 if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
392 logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
393 distributionId,
394 keyCreateTime,
395 keyAge,
396 TimeUnit.MILLISECONDS.toDays(keyAge));
397 account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
398 }
399
400 List<SignalServiceAddress> addresses = recipientIdList.stream()
401 .map(addressResolver::resolveSignalServiceAddress)
402 .collect(Collectors.toList());
403 List<UnidentifiedAccess> unidentifiedAccesses = recipientIdList.stream()
404 .map(unidentifiedAccessHelper::getAccessFor)
405 .map(Optional::get)
406 .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
407 .map(Optional::get)
408 .collect(Collectors.toList());
409
410 try {
411 List<SendMessageResult> results = messageSender.sendGroupDataMessage(distributionId,
412 addresses,
413 unidentifiedAccesses,
414 isRecipientUpdate,
415 ContentHint.DEFAULT,
416 message,
417 SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY);
418
419 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
420 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
421 successCount,
422 addresses.size());
423
424 return results;
425 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
426 return null;
427 } catch (InvalidUnidentifiedAccessHeaderException e) {
428 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
429 return null;
430 } catch (NoSessionException e) {
431 logger.warn("No session. Falling back to legacy sends.", e);
432 account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
433 return null;
434 } catch (InvalidKeyException e) {
435 logger.warn("Invalid key. Falling back to legacy sends.", e);
436 account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
437 return null;
438 } catch (InvalidRegistrationIdException e) {
439 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
440 return null;
441 } catch (NotFoundException e) {
442 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
443 return null;
444 }
445 }
446
447 private SendMessageResult sendMessage(
448 SignalServiceDataMessage message, RecipientId recipientId
449 ) {
450 return handleSendMessage(recipientId,
451 (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
452 unidentifiedAccess,
453 ContentHint.DEFAULT,
454 message,
455 SignalServiceMessageSender.IndividualSendEvents.EMPTY));
456 }
457
458 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
459 var messageSender = dependencies.getMessageSender();
460
461 var address = addressResolver.resolveSignalServiceAddress(recipientId);
462 try {
463 try {
464 return s.send(messageSender, address, unidentifiedAccessHelper.getAccessFor(recipientId));
465 } catch (UnregisteredUserException e) {
466 final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
467 address = addressResolver.resolveSignalServiceAddress(newRecipientId);
468 return s.send(messageSender, address, unidentifiedAccessHelper.getAccessFor(newRecipientId));
469 }
470 } catch (UnregisteredUserException e) {
471 return SendMessageResult.unregisteredFailure(address);
472 } catch (ProofRequiredException e) {
473 return SendMessageResult.proofRequiredFailure(address, e);
474 } catch (RateLimitException e) {
475 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
476 return SendMessageResult.networkFailure(address);
477 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
478 return SendMessageResult.identityFailure(address, e.getIdentityKey());
479 } catch (IOException e) {
480 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
481 return SendMessageResult.networkFailure(address);
482 }
483 }
484
485 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) {
486 var address = account.getSelfAddress();
487 var transcript = new SentTranscriptMessage(Optional.of(address),
488 message.getTimestamp(),
489 message,
490 message.getExpiresInSeconds(),
491 Map.of(address, true),
492 false);
493 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
494
495 return sendSyncMessage(syncMessage);
496 }
497
498 private void handleSendMessageResult(final SendMessageResult r) {
499 if (r.getIdentityFailure() != null) {
500 final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
501 identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure());
502 }
503 }
504
505 interface SenderHandler {
506
507 SendMessageResult send(
508 SignalServiceMessageSender messageSender,
509 SignalServiceAddress address,
510 Optional<UnidentifiedAccessPair> unidentifiedAccess
511 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
512 }
513 }