]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
Update libsignal-service
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / SendHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.ByteString;
4
5 import org.asamk.signal.manager.api.Contact;
6 import org.asamk.signal.manager.api.GroupId;
7 import org.asamk.signal.manager.api.GroupNotFoundException;
8 import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
9 import org.asamk.signal.manager.api.NotAGroupMemberException;
10 import org.asamk.signal.manager.api.Profile;
11 import org.asamk.signal.manager.api.UnregisteredRecipientException;
12 import org.asamk.signal.manager.groups.GroupUtils;
13 import org.asamk.signal.manager.internal.SignalDependencies;
14 import org.asamk.signal.manager.storage.SignalAccount;
15 import org.asamk.signal.manager.storage.groups.GroupInfo;
16 import org.asamk.signal.manager.storage.recipients.RecipientId;
17 import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
18 import org.signal.libsignal.protocol.InvalidKeyException;
19 import org.signal.libsignal.protocol.InvalidRegistrationIdException;
20 import org.signal.libsignal.protocol.NoSessionException;
21 import org.signal.libsignal.protocol.SignalProtocolAddress;
22 import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
26 import org.whispersystems.signalservice.api.crypto.ContentHint;
27 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
28 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
29 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
30 import org.whispersystems.signalservice.api.messages.SendMessageResult;
31 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
32 import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
33 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
34 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
35 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
36 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
37 import org.whispersystems.signalservice.api.push.DistributionId;
38 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
39 import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
40 import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
41 import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
42 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
43 import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
44 import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener;
45
46 import java.io.IOException;
47 import java.time.Duration;
48 import java.util.ArrayList;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.concurrent.TimeUnit;
55 import java.util.concurrent.atomic.AtomicLong;
56
57 public class SendHelper {
58
59 private final static Logger logger = LoggerFactory.getLogger(SendHelper.class);
60
61 private final SignalAccount account;
62 private final SignalDependencies dependencies;
63 private final Context context;
64
65 public SendHelper(final Context context) {
66 this.account = context.getAccount();
67 this.dependencies = context.getDependencies();
68 this.context = context;
69 }
70
71 /**
72 * Send a single message to one recipient.
73 * The message is extended with the current expiration timer.
74 */
75 public SendMessageResult sendMessage(
76 final SignalServiceDataMessage.Builder messageBuilder,
77 final RecipientId recipientId,
78 Optional<Long> editTargetTimestamp
79 ) {
80 var contact = account.getContactStore().getContact(recipientId);
81 if (contact == null || !contact.isProfileSharingEnabled()) {
82 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
83 contact = contactBuilder.withProfileSharingEnabled(true).build();
84 account.getContactStore().storeContact(recipientId, contact);
85 }
86
87 final var expirationTime = contact.getMessageExpirationTime();
88 messageBuilder.withExpiration(expirationTime);
89
90 if (!contact.isBlocked()) {
91 final var profileKey = account.getProfileKey().serialize();
92 messageBuilder.withProfileKey(profileKey);
93 }
94
95 final var message = messageBuilder.build();
96 return sendMessage(message, recipientId, editTargetTimestamp);
97 }
98
99 /**
100 * Send a group message to the given group
101 * The message is extended with the current expiration timer for the group and the group context.
102 */
103 public List<SendMessageResult> sendAsGroupMessage(
104 SignalServiceDataMessage.Builder messageBuilder, GroupId groupId, Optional<Long> editTargetTimestamp
105 ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
106 final var g = getGroupForSending(groupId);
107 return sendAsGroupMessage(messageBuilder, g, editTargetTimestamp);
108 }
109
110 /**
111 * Send a complete group message to the given recipients (should be current/old/new members)
112 * This method should only be used for create/update/quit group messages.
113 */
114 public List<SendMessageResult> sendGroupMessage(
115 final SignalServiceDataMessage message,
116 final Set<RecipientId> recipientIds,
117 final DistributionId distributionId
118 ) throws IOException {
119 return sendGroupMessage(message, recipientIds, distributionId, ContentHint.IMPLICIT, Optional.empty());
120 }
121
122 public SendMessageResult sendReceiptMessage(
123 final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
124 ) {
125 final var messageSendLogStore = account.getMessageSendLogStore();
126 final var result = handleSendMessage(recipientId,
127 (messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
128 unidentifiedAccess,
129 receiptMessage,
130 false));
131 messageSendLogStore.insertIfPossible(receiptMessage.getWhen(), result, ContentHint.IMPLICIT, false);
132 handleSendMessageResult(result);
133 return result;
134 }
135
136 public SendMessageResult sendProfileKey(RecipientId recipientId) {
137 logger.debug("Sending updated profile key to recipient: {}", recipientId);
138 final var profileKey = account.getProfileKey().serialize();
139 final var message = SignalServiceDataMessage.newBuilder()
140 .asProfileKeyUpdate(true)
141 .withProfileKey(profileKey)
142 .build();
143 return handleSendMessage(recipientId,
144 (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
145 unidentifiedAccess,
146 ContentHint.IMPLICIT,
147 message,
148 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
149 false,
150 false));
151 }
152
153 public SendMessageResult sendRetryReceipt(
154 DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
155 ) {
156 logger.debug("Sending retry receipt for {} to {}, device: {}",
157 errorMessage.getTimestamp(),
158 recipientId,
159 errorMessage.getDeviceId());
160 final var result = handleSendMessage(recipientId,
161 (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
162 unidentifiedAccess,
163 groupId.map(GroupId::serialize),
164 errorMessage));
165 handleSendMessageResult(result);
166 return result;
167 }
168
169 public SendMessageResult sendNullMessage(RecipientId recipientId) {
170 final var result = handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
171 handleSendMessageResult(result);
172 return result;
173 }
174
175 public SendMessageResult sendSelfMessage(
176 SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
177 ) {
178 final var recipientId = account.getSelfRecipientId();
179 final var contact = account.getContactStore().getContact(recipientId);
180 final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
181 messageBuilder.withExpiration(expirationTime);
182
183 var message = messageBuilder.build();
184 return sendSelfMessage(message, editTargetTimestamp);
185 }
186
187 public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
188 var messageSender = dependencies.getMessageSender();
189 try {
190 return messageSender.sendSyncMessage(message, context.getUnidentifiedAccessHelper().getAccessForSync());
191 } catch (UnregisteredUserException e) {
192 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
193 return SendMessageResult.unregisteredFailure(address);
194 } catch (ProofRequiredException e) {
195 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
196 return SendMessageResult.proofRequiredFailure(address, e);
197 } catch (RateLimitException e) {
198 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
199 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
200 return SendMessageResult.rateLimitFailure(address, e);
201 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
202 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
203 return SendMessageResult.identityFailure(address, e.getIdentityKey());
204 } catch (IOException e) {
205 var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
206 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
207 logger.debug("Exception", e);
208 return SendMessageResult.networkFailure(address);
209 }
210 }
211
212 public SendMessageResult sendTypingMessage(
213 SignalServiceTypingMessage message, RecipientId recipientId
214 ) {
215 final var result = handleSendMessage(recipientId,
216 (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(List.of(address),
217 List.of(unidentifiedAccess),
218 message,
219 null).get(0));
220 handleSendMessageResult(result);
221 return result;
222 }
223
224 public List<SendMessageResult> sendGroupTypingMessage(
225 SignalServiceTypingMessage message, GroupId groupId
226 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
227 final var g = getGroupForSending(groupId);
228 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
229 throw new GroupSendingNotAllowedException(groupId, g.getTitle());
230 }
231 final var distributionId = g.getDistributionId();
232 final var recipientIds = g.getMembersWithout(account.getSelfRecipientId());
233
234 return sendGroupTypingMessage(message, recipientIds, distributionId);
235 }
236
237 public SendMessageResult resendMessage(
238 final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
239 ) {
240 logger.trace("Resending message {} to {}", timestamp, recipientId);
241 if (messageSendLogEntry.groupId().isEmpty()) {
242 return handleSendMessage(recipientId,
243 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
244 unidentifiedAccess,
245 timestamp,
246 messageSendLogEntry.content(),
247 messageSendLogEntry.contentHint(),
248 Optional.empty(),
249 messageSendLogEntry.urgent()));
250 }
251
252 final var groupId = messageSendLogEntry.groupId().get();
253 final var group = account.getGroupStore().getGroup(groupId);
254
255 if (group == null) {
256 logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
257 groupId.toBase64());
258 return null;
259 } else if (!group.getMembers().contains(recipientId)) {
260 logger.warn("The target user is no longer in the group {}! Skipping message send.", groupId.toBase64());
261 return null;
262 }
263
264 final var senderKeyDistributionMessage = dependencies.getMessageSender()
265 .getOrCreateNewGroupSession(group.getDistributionId());
266 final var distributionBytes = ByteString.copyFrom(senderKeyDistributionMessage.serialize());
267 final var contentToSend = messageSendLogEntry.content()
268 .toBuilder()
269 .setSenderKeyDistributionMessage(distributionBytes)
270 .build();
271
272 final var result = handleSendMessage(recipientId,
273 (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
274 unidentifiedAccess,
275 timestamp,
276 contentToSend,
277 messageSendLogEntry.contentHint(),
278 Optional.of(group.getGroupId().serialize()),
279 messageSendLogEntry.urgent()));
280
281 if (result.isSuccess()) {
282 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
283 final var addresses = result.getSuccess()
284 .getDevices()
285 .stream()
286 .map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
287 .toList();
288
289 account.getSenderKeyStore().markSenderKeySharedWith(group.getDistributionId(), addresses);
290 }
291
292 return result;
293 }
294
295 private List<SendMessageResult> sendAsGroupMessage(
296 final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g, Optional<Long> editTargetTimestamp
297 ) throws IOException, GroupSendingNotAllowedException {
298 GroupUtils.setGroupContext(messageBuilder, g);
299 messageBuilder.withExpiration(g.getMessageExpirationTimer());
300
301 final var message = messageBuilder.build();
302 final var recipients = g.getMembersWithout(account.getSelfRecipientId());
303
304 if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
305 if (message.getBody().isPresent()
306 || message.getAttachments().isPresent()
307 || message.getQuote().isPresent()
308 || message.getPreviews().isPresent()
309 || message.getMentions().isPresent()
310 || message.getSticker().isPresent()) {
311 throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
312 }
313 }
314
315 return sendGroupMessage(message,
316 recipients,
317 g.getDistributionId(),
318 ContentHint.RESENDABLE,
319 editTargetTimestamp);
320 }
321
322 private List<SendMessageResult> sendGroupMessage(
323 final SignalServiceDataMessage message,
324 final Set<RecipientId> recipientIds,
325 final DistributionId distributionId,
326 final ContentHint contentHint,
327 final Optional<Long> editTargetTimestamp
328 ) throws IOException {
329 final var messageSender = dependencies.getMessageSender();
330 final var messageSendLogStore = account.getMessageSendLogStore();
331 final AtomicLong entryId = new AtomicLong(-1);
332
333 final var urgent = true;
334 final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
335 logger.trace("Partial message send result: {}", sendResult.isSuccess());
336 synchronized (entryId) {
337 if (entryId.get() == -1) {
338 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
339 sendResult,
340 contentHint,
341 urgent);
342 entryId.set(newId);
343 } else {
344 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
345 }
346 }
347 };
348 final LegacySenderHandler legacySender = (recipients, unidentifiedAccess, isRecipientUpdate) ->
349 editTargetTimestamp.isEmpty()
350 ? messageSender.sendDataMessage(recipients,
351 unidentifiedAccess,
352 isRecipientUpdate,
353 contentHint,
354 message,
355 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
356 partialSendCompleteListener,
357 () -> false,
358 urgent)
359 : messageSender.sendEditMessage(recipients,
360 unidentifiedAccess,
361 isRecipientUpdate,
362 contentHint,
363 message,
364 SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
365 partialSendCompleteListener,
366 () -> false,
367 urgent,
368 editTargetTimestamp.get());
369 final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
370 distId,
371 recipients,
372 unidentifiedAccess,
373 isRecipientUpdate,
374 contentHint,
375 message,
376 SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY,
377 urgent,
378 false,
379 editTargetTimestamp.map(timestamp -> new SignalServiceEditMessage(timestamp, message)).orElse(null),
380 sendResult -> {
381 logger.trace("Partial message send results: {}", sendResult.size());
382 synchronized (entryId) {
383 if (entryId.get() == -1) {
384 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
385 sendResult,
386 contentHint,
387 urgent);
388 entryId.set(newId);
389 } else {
390 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
391 }
392 }
393 synchronized (entryId) {
394 if (entryId.get() == -1) {
395 final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
396 sendResult,
397 contentHint,
398 urgent);
399 entryId.set(newId);
400 } else {
401 messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
402 }
403 }
404 });
405 final var results = sendGroupMessageInternal(legacySender, senderKeySender, recipientIds, distributionId);
406
407 for (var r : results) {
408 handleSendMessageResult(r);
409 }
410
411 return results;
412 }
413
414 private List<SendMessageResult> sendGroupTypingMessage(
415 final SignalServiceTypingMessage message,
416 final Set<RecipientId> recipientIds,
417 final DistributionId distributionId
418 ) throws IOException {
419 final var messageSender = dependencies.getMessageSender();
420 final var results = sendGroupMessageInternal((recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendTyping(
421 recipients,
422 unidentifiedAccess,
423 message,
424 () -> false),
425 (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupTyping(distId,
426 recipients,
427 unidentifiedAccess,
428 message),
429 recipientIds,
430 distributionId);
431
432 for (var r : results) {
433 handleSendMessageResult(r);
434 }
435
436 return results;
437 }
438
439 private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
440 var g = context.getGroupHelper().getGroup(groupId);
441 if (g == null) {
442 throw new GroupNotFoundException(groupId);
443 }
444 if (!g.isMember(account.getSelfRecipientId())) {
445 throw new NotAGroupMemberException(groupId, g.getTitle());
446 }
447 return g;
448 }
449
450 private List<SendMessageResult> sendGroupMessageInternal(
451 final LegacySenderHandler legacySender,
452 final SenderKeySenderHandler senderKeySender,
453 final Set<RecipientId> recipientIds,
454 final DistributionId distributionId
455 ) throws IOException {
456 long startTime = System.currentTimeMillis();
457 // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
458 final var isRecipientUpdate = false;
459 Set<RecipientId> senderKeyTargets = distributionId == null
460 ? Set.of()
461 : getSenderKeyCapableRecipientIds(recipientIds);
462 final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
463
464 if (senderKeyTargets.size() > 0) {
465 final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
466 senderKeyTargets,
467 distributionId,
468 isRecipientUpdate);
469
470 if (results == null) {
471 senderKeyTargets = Set.of();
472 } else {
473 results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
474 final var failedTargets = results.stream()
475 .filter(r -> !r.isSuccess())
476 .map(r -> context.getRecipientHelper().resolveRecipient(r.getAddress()))
477 .toList();
478 if (failedTargets.size() > 0) {
479 senderKeyTargets = new HashSet<>(senderKeyTargets);
480 failedTargets.forEach(senderKeyTargets::remove);
481 }
482 }
483 }
484
485 final var legacyTargets = new HashSet<>(recipientIds);
486 legacyTargets.removeAll(senderKeyTargets);
487 final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
488
489 if (legacyTargets.size() > 0 || onlyTargetIsSelfWithLinkedDevice) {
490 if (legacyTargets.size() > 0) {
491 logger.debug("Need to do {} legacy sends.", legacyTargets.size());
492 } else {
493 logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
494 }
495
496 final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(legacySender,
497 legacyTargets,
498 isRecipientUpdate || allResults.size() > 0);
499 allResults.addAll(results);
500 }
501 final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
502 logger.debug("Sending took {}", duration.toString());
503 return allResults;
504 }
505
506 private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
507 final var selfProfile = context.getProfileHelper().getSelfProfile();
508 if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
509 logger.debug("Not all of our devices support sender key. Using legacy.");
510 return Set.of();
511 }
512
513 final var senderKeyTargets = new HashSet<RecipientId>();
514 final var recipientList = new ArrayList<>(recipientIds);
515 final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
516 for (final var recipientId : recipientList) {
517 final var profile = profiles.next();
518 if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
519 continue;
520 }
521
522 final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
523 if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
524 continue;
525 }
526
527 final var serviceId = account.getRecipientAddressResolver()
528 .resolveRecipientAddress(recipientId)
529 .serviceId()
530 .orElse(null);
531 if (serviceId == null) {
532 continue;
533 }
534 final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
535 if (identity == null || !identity.getTrustLevel().isTrusted()) {
536 continue;
537 }
538
539 senderKeyTargets.add(recipientId);
540 }
541
542 if (senderKeyTargets.size() < 2) {
543 logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
544 return Set.of();
545 }
546
547 logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
548 return senderKeyTargets;
549 }
550
551 private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
552 final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
553 ) throws IOException {
554 final var recipientIdList = new ArrayList<>(recipientIds);
555 final var addresses = recipientIdList.stream()
556 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
557 .toList();
558 final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList);
559 try {
560 final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
561
562 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
563 logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
564 return results;
565 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
566 return List.of();
567 }
568 }
569
570 private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
571 final SenderKeySenderHandler sender,
572 final Set<RecipientId> recipientIds,
573 final DistributionId distributionId,
574 final boolean isRecipientUpdate
575 ) throws IOException {
576 final var recipientIdList = new ArrayList<>(recipientIds);
577
578 long keyCreateTime = account.getSenderKeyStore()
579 .getCreateTimeForOurKey(account.getAci(), account.getDeviceId(), distributionId);
580 long keyAge = System.currentTimeMillis() - keyCreateTime;
581
582 if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
583 logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
584 distributionId,
585 keyCreateTime,
586 keyAge,
587 TimeUnit.MILLISECONDS.toDays(keyAge));
588 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
589 }
590
591 List<SignalServiceAddress> addresses = recipientIdList.stream()
592 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
593 .toList();
594 List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
595 .getAccessFor(recipientIdList)
596 .stream()
597 .map(Optional::get)
598 .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
599 .map(Optional::get)
600 .toList();
601
602 try {
603 List<SendMessageResult> results = sender.send(distributionId,
604 addresses,
605 unidentifiedAccesses,
606 isRecipientUpdate);
607
608 final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
609 logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
610 successCount,
611 addresses.size());
612
613 return results;
614 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
615 return null;
616 } catch (InvalidUnidentifiedAccessHeaderException e) {
617 logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
618 return null;
619 } catch (NoSessionException e) {
620 logger.warn("No session. Falling back to legacy sends.", e);
621 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
622 return null;
623 } catch (InvalidKeyException e) {
624 logger.warn("Invalid key. Falling back to legacy sends.", e);
625 account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
626 return null;
627 } catch (InvalidRegistrationIdException e) {
628 logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
629 return null;
630 } catch (NotFoundException e) {
631 logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
632 return null;
633 } catch (IOException e) {
634 if (e.getCause() instanceof InvalidKeyException) {
635 logger.warn("Invalid key. Falling back to legacy sends.", e);
636 return null;
637 } else {
638 throw e;
639 }
640 }
641 }
642
643 private SendMessageResult sendMessage(
644 SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
645 ) {
646 final var messageSendLogStore = account.getMessageSendLogStore();
647 final var urgent = true;
648 final var includePniSignature = false;
649 final var result = handleSendMessage(recipientId,
650 editTargetTimestamp.isEmpty()
651 ? (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
652 unidentifiedAccess,
653 ContentHint.RESENDABLE,
654 message,
655 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
656 urgent,
657 includePniSignature)
658 : (messageSender, address, unidentifiedAccess) -> messageSender.sendEditMessage(address,
659 unidentifiedAccess,
660 ContentHint.RESENDABLE,
661 message,
662 SignalServiceMessageSender.IndividualSendEvents.EMPTY,
663 urgent,
664 editTargetTimestamp.get()));
665 messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE, urgent);
666 handleSendMessageResult(result);
667 return result;
668 }
669
670 private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
671 var messageSender = dependencies.getMessageSender();
672
673 var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
674 try {
675 try {
676 return s.send(messageSender, address, context.getUnidentifiedAccessHelper().getAccessFor(recipientId));
677 } catch (UnregisteredUserException e) {
678 final RecipientId newRecipientId;
679 try {
680 newRecipientId = context.getRecipientHelper().refreshRegisteredUser(recipientId);
681 } catch (UnregisteredRecipientException ex) {
682 return SendMessageResult.unregisteredFailure(address);
683 }
684 address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
685 return s.send(messageSender,
686 address,
687 context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId));
688 }
689 } catch (UnregisteredUserException e) {
690 return SendMessageResult.unregisteredFailure(address);
691 } catch (ProofRequiredException e) {
692 return SendMessageResult.proofRequiredFailure(address, e);
693 } catch (RateLimitException e) {
694 logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage());
695 return SendMessageResult.rateLimitFailure(address, e);
696 } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
697 return SendMessageResult.identityFailure(address, e.getIdentityKey());
698 } catch (IOException e) {
699 logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
700 logger.debug("Exception", e);
701 return SendMessageResult.networkFailure(address);
702 }
703 }
704
705 private SendMessageResult sendSelfMessage(SignalServiceDataMessage message, Optional<Long> editTargetTimestamp) {
706 var address = account.getSelfAddress();
707 var transcript = new SentTranscriptMessage(Optional.of(address),
708 message.getTimestamp(),
709 editTargetTimestamp.isEmpty() ? Optional.of(message) : Optional.empty(),
710 message.getExpiresInSeconds(),
711 Map.of(address.getServiceId(), true),
712 false,
713 Optional.empty(),
714 Set.of(),
715 editTargetTimestamp.map((timestamp) -> new SignalServiceEditMessage(timestamp, message)));
716 var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
717
718 return sendSyncMessage(syncMessage);
719 }
720
721 private void handleSendMessageResult(final SendMessageResult r) {
722 if (r.isSuccess() && !r.getSuccess().isUnidentified()) {
723 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
724 final var profile = account.getProfileStore().getProfile(recipientId);
725 if (profile != null && (
726 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
727 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
728 )) {
729 account.getProfileStore()
730 .storeProfile(recipientId,
731 Profile.newBuilder(profile)
732 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
733 .build());
734 }
735 }
736 if (r.isUnregisteredFailure()) {
737 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
738 final var profile = account.getProfileStore().getProfile(recipientId);
739 if (profile != null && (
740 profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.ENABLED
741 || profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED
742 )) {
743 account.getProfileStore()
744 .storeProfile(recipientId,
745 Profile.newBuilder(profile)
746 .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
747 .build());
748 }
749 }
750 if (r.getIdentityFailure() != null) {
751 final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
752 context.getIdentityHelper()
753 .handleIdentityFailure(recipientId, r.getAddress().getServiceId(), r.getIdentityFailure());
754 }
755 }
756
757 interface SenderHandler {
758
759 SendMessageResult send(
760 SignalServiceMessageSender messageSender,
761 SignalServiceAddress address,
762 Optional<UnidentifiedAccessPair> unidentifiedAccess
763 ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
764 }
765
766 interface SenderKeySenderHandler {
767
768 List<SendMessageResult> send(
769 DistributionId distributionId,
770 List<SignalServiceAddress> recipients,
771 List<UnidentifiedAccess> unidentifiedAccess,
772 boolean isRecipientUpdate
773 ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
774 }
775
776 interface LegacySenderHandler {
777
778 List<SendMessageResult> send(
779 List<SignalServiceAddress> recipients,
780 List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
781 boolean isRecipientUpdate
782 ) throws IOException, UntrustedIdentityException;
783 }
784 }