]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
e6e434787e3af3ed97fef9ca6c2a487bd4ef158b
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / IncomingMessageHandler.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.JobExecutor;
4 import org.asamk.signal.manager.Manager;
5 import org.asamk.signal.manager.SignalDependencies;
6 import org.asamk.signal.manager.TrustLevel;
7 import org.asamk.signal.manager.UntrustedIdentityException;
8 import org.asamk.signal.manager.actions.HandleAction;
9 import org.asamk.signal.manager.actions.RenewSessionAction;
10 import org.asamk.signal.manager.actions.RetrieveProfileAction;
11 import org.asamk.signal.manager.actions.SendGroupInfoAction;
12 import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
13 import org.asamk.signal.manager.actions.SendReceiptAction;
14 import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
15 import org.asamk.signal.manager.actions.SendSyncContactsAction;
16 import org.asamk.signal.manager.actions.SendSyncGroupsAction;
17 import org.asamk.signal.manager.groups.GroupId;
18 import org.asamk.signal.manager.groups.GroupNotFoundException;
19 import org.asamk.signal.manager.groups.GroupUtils;
20 import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
21 import org.asamk.signal.manager.storage.SignalAccount;
22 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
23 import org.asamk.signal.manager.storage.recipients.RecipientId;
24 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
25 import org.asamk.signal.manager.storage.stickers.Sticker;
26 import org.asamk.signal.manager.storage.stickers.StickerPackId;
27 import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
28 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
29 import org.signal.zkgroup.InvalidInputException;
30 import org.signal.zkgroup.profiles.ProfileKey;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.whispersystems.libsignal.SignalProtocolAddress;
34 import org.whispersystems.libsignal.util.Pair;
35 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
36 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
37 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
38 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
39 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
40 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
41 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
42
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.stream.Collectors;
46
47 public final class IncomingMessageHandler {
48
49 private final static Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class);
50
51 private final SignalAccount account;
52 private final SignalDependencies dependencies;
53 private final RecipientResolver recipientResolver;
54 private final SignalServiceAddressResolver addressResolver;
55 private final GroupHelper groupHelper;
56 private final ContactHelper contactHelper;
57 private final AttachmentHelper attachmentHelper;
58 private final SyncHelper syncHelper;
59 private final JobExecutor jobExecutor;
60
61 public IncomingMessageHandler(
62 final SignalAccount account,
63 final SignalDependencies dependencies,
64 final RecipientResolver recipientResolver,
65 final SignalServiceAddressResolver addressResolver,
66 final GroupHelper groupHelper,
67 final ContactHelper contactHelper,
68 final AttachmentHelper attachmentHelper,
69 final SyncHelper syncHelper,
70 final JobExecutor jobExecutor
71 ) {
72 this.account = account;
73 this.dependencies = dependencies;
74 this.recipientResolver = recipientResolver;
75 this.addressResolver = addressResolver;
76 this.groupHelper = groupHelper;
77 this.contactHelper = contactHelper;
78 this.attachmentHelper = attachmentHelper;
79 this.syncHelper = syncHelper;
80 this.jobExecutor = jobExecutor;
81 }
82
83 public Pair<List<HandleAction>, Exception> handleRetryEnvelope(
84 final SignalServiceEnvelope envelope,
85 final boolean ignoreAttachments,
86 final Manager.ReceiveMessageHandler handler
87 ) {
88 SignalServiceContent content = null;
89 if (!envelope.isReceipt()) {
90 try {
91 content = dependencies.getCipher().decrypt(envelope);
92 } catch (ProtocolUntrustedIdentityException e) {
93 final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender());
94 final var exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(
95 recipientId), e.getSenderDevice());
96 return new Pair<>(List.of(), exception);
97 } catch (Exception e) {
98 return new Pair<>(List.of(), e);
99 }
100 }
101 final var actions = checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null);
102 return new Pair<>(actions, null);
103 }
104
105 public Pair<List<HandleAction>, Exception> handleEnvelope(
106 final SignalServiceEnvelope envelope,
107 final boolean ignoreAttachments,
108 final Manager.ReceiveMessageHandler handler
109 ) {
110 final var actions = new ArrayList<HandleAction>();
111 if (envelope.hasSourceUuid()) {
112 // Store uuid if we don't have it already
113 // address/uuid in envelope is sent by server
114 account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress());
115 }
116 SignalServiceContent content = null;
117 Exception exception = null;
118 if (!envelope.isReceipt()) {
119 try {
120 content = dependencies.getCipher().decrypt(envelope);
121 } catch (ProtocolUntrustedIdentityException e) {
122 final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender());
123 actions.add(new RetrieveProfileAction(recipientId));
124 exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(recipientId),
125 e.getSenderDevice());
126 } catch (ProtocolInvalidMessageException e) {
127 final var sender = account.getRecipientStore().resolveRecipient(e.getSender());
128 logger.debug("Received invalid message, queuing renew session action.");
129 actions.add(new RenewSessionAction(sender));
130 exception = e;
131 } catch (Exception e) {
132 exception = e;
133 }
134 }
135
136 actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, exception));
137 return new Pair<>(actions, exception);
138 }
139
140 private List<HandleAction> checkAndHandleMessage(
141 final SignalServiceEnvelope envelope,
142 final SignalServiceContent content,
143 final boolean ignoreAttachments,
144 final Manager.ReceiveMessageHandler handler,
145 final Exception exception
146 ) {
147 if (!envelope.hasSourceUuid() && content != null) {
148 // Store uuid if we don't have it already
149 // address/uuid is validated by unidentified sender certificate
150 account.getRecipientStore().resolveRecipientTrusted(content.getSender());
151 }
152 if (isMessageBlocked(envelope, content)) {
153 logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
154 return List.of();
155 } else if (isNotAllowedToSendToGroup(envelope, content)) {
156 logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
157 (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(),
158 envelope.getTimestamp());
159 return List.of();
160 } else {
161 List<HandleAction> actions;
162 if (content != null) {
163 actions = handleMessage(envelope, content, ignoreAttachments);
164 } else {
165 actions = List.of();
166 }
167 handler.handleMessage(envelope, content, exception);
168 return actions;
169 }
170 }
171
172 public List<HandleAction> handleMessage(
173 SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments
174 ) {
175 var actions = new ArrayList<HandleAction>();
176 final RecipientId sender;
177 final int senderDeviceId;
178 if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
179 sender = recipientResolver.resolveRecipient(envelope.getSourceAddress());
180 senderDeviceId = envelope.getSourceDevice();
181 } else {
182 sender = recipientResolver.resolveRecipient(content.getSender());
183 senderDeviceId = content.getSenderDevice();
184 }
185
186 if (content.getSenderKeyDistributionMessage().isPresent()) {
187 final var message = content.getSenderKeyDistributionMessage().get();
188 final var protocolAddress = new SignalProtocolAddress(addressResolver.resolveSignalServiceAddress(sender)
189 .getIdentifier(), senderDeviceId);
190 dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message);
191 }
192
193 if (content.getDataMessage().isPresent()) {
194 var message = content.getDataMessage().get();
195
196 if (content.isNeedsReceipt()) {
197 actions.add(new SendReceiptAction(sender, message.getTimestamp()));
198 }
199
200 actions.addAll(handleSignalServiceDataMessage(message,
201 false,
202 sender,
203 account.getSelfRecipientId(),
204 ignoreAttachments));
205 }
206
207 if (content.getSyncMessage().isPresent()) {
208 var syncMessage = content.getSyncMessage().get();
209 actions.addAll(handleSyncMessage(syncMessage, sender, ignoreAttachments));
210 }
211
212 return actions;
213 }
214
215 private List<HandleAction> handleSyncMessage(
216 final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments
217 ) {
218 var actions = new ArrayList<HandleAction>();
219 account.setMultiDevice(true);
220 if (syncMessage.getSent().isPresent()) {
221 var message = syncMessage.getSent().get();
222 final var destination = message.getDestination().orNull();
223 actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
224 true,
225 sender,
226 destination == null ? null : recipientResolver.resolveRecipient(destination),
227 ignoreAttachments));
228 }
229 if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) {
230 var rm = syncMessage.getRequest().get();
231 if (rm.isContactsRequest()) {
232 actions.add(SendSyncContactsAction.create());
233 }
234 if (rm.isGroupsRequest()) {
235 actions.add(SendSyncGroupsAction.create());
236 }
237 if (rm.isBlockedListRequest()) {
238 actions.add(SendSyncBlockedListAction.create());
239 }
240 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
241 }
242 if (syncMessage.getGroups().isPresent()) {
243 logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
244 }
245 if (syncMessage.getBlockedList().isPresent()) {
246 final var blockedListMessage = syncMessage.getBlockedList().get();
247 for (var address : blockedListMessage.getAddresses()) {
248 contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true);
249 }
250 for (var groupId : blockedListMessage.getGroupIds()
251 .stream()
252 .map(GroupId::unknownVersion)
253 .collect(Collectors.toSet())) {
254 try {
255 groupHelper.setGroupBlocked(groupId, true);
256 } catch (GroupNotFoundException e) {
257 logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
258 groupId.toBase64());
259 }
260 }
261 }
262 if (syncMessage.getContacts().isPresent()) {
263 try {
264 final var contactsMessage = syncMessage.getContacts().get();
265 attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(),
266 syncHelper::handleSyncDeviceContacts);
267 } catch (Exception e) {
268 logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage());
269 }
270 }
271 if (syncMessage.getVerified().isPresent()) {
272 final var verifiedMessage = syncMessage.getVerified().get();
273 account.getIdentityKeyStore()
274 .setIdentityTrustLevel(account.getRecipientStore()
275 .resolveRecipientTrusted(verifiedMessage.getDestination()),
276 verifiedMessage.getIdentityKey(),
277 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
278 }
279 if (syncMessage.getStickerPackOperations().isPresent()) {
280 final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
281 for (var m : stickerPackOperationMessages) {
282 if (!m.getPackId().isPresent()) {
283 continue;
284 }
285 final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
286 final var installed = !m.getType().isPresent()
287 || m.getType().get() == StickerPackOperationMessage.Type.INSTALL;
288
289 var sticker = account.getStickerStore().getSticker(stickerPackId);
290 if (m.getPackKey().isPresent()) {
291 if (sticker == null) {
292 sticker = new Sticker(stickerPackId, m.getPackKey().get());
293 }
294 if (installed) {
295 jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get()));
296 }
297 }
298
299 if (sticker != null) {
300 sticker.setInstalled(installed);
301 account.getStickerStore().updateSticker(sticker);
302 }
303 }
304 }
305 if (syncMessage.getFetchType().isPresent()) {
306 switch (syncMessage.getFetchType().get()) {
307 case LOCAL_PROFILE:
308 actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
309 case STORAGE_MANIFEST:
310 // TODO
311 }
312 }
313 if (syncMessage.getKeys().isPresent()) {
314 final var keysMessage = syncMessage.getKeys().get();
315 if (keysMessage.getStorageService().isPresent()) {
316 final var storageKey = keysMessage.getStorageService().get();
317 account.setStorageKey(storageKey);
318 }
319 }
320 if (syncMessage.getConfiguration().isPresent()) {
321 // TODO
322 }
323 return actions;
324 }
325
326 private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
327 SignalServiceAddress source;
328 if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
329 source = envelope.getSourceAddress();
330 } else if (content != null) {
331 source = content.getSender();
332 } else {
333 return false;
334 }
335 final var recipientId = recipientResolver.resolveRecipient(source);
336 if (contactHelper.isContactBlocked(recipientId)) {
337 return true;
338 }
339
340 if (content != null && content.getDataMessage().isPresent()) {
341 var message = content.getDataMessage().get();
342 if (message.getGroupContext().isPresent()) {
343 var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
344 return groupHelper.isGroupBlocked(groupId);
345 }
346 }
347
348 return false;
349 }
350
351 private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) {
352 SignalServiceAddress source;
353 if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
354 source = envelope.getSourceAddress();
355 } else if (content != null) {
356 source = content.getSender();
357 } else {
358 return false;
359 }
360
361 if (content == null || !content.getDataMessage().isPresent()) {
362 return false;
363 }
364
365 var message = content.getDataMessage().get();
366 if (!message.getGroupContext().isPresent()) {
367 return false;
368 }
369
370 if (message.getGroupContext().get().getGroupV1().isPresent()) {
371 var groupInfo = message.getGroupContext().get().getGroupV1().get();
372 if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
373 return false;
374 }
375 }
376
377 var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
378 var group = groupHelper.getGroup(groupId);
379 if (group == null) {
380 return false;
381 }
382
383 final var recipientId = recipientResolver.resolveRecipient(source);
384 if (!group.isMember(recipientId)) {
385 return true;
386 }
387
388 if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
389 return message.getBody().isPresent()
390 || message.getAttachments().isPresent()
391 || message.getQuote()
392 .isPresent()
393 || message.getPreviews().isPresent()
394 || message.getMentions().isPresent()
395 || message.getSticker().isPresent();
396 }
397 return false;
398 }
399
400 private List<HandleAction> handleSignalServiceDataMessage(
401 SignalServiceDataMessage message,
402 boolean isSync,
403 RecipientId source,
404 RecipientId destination,
405 boolean ignoreAttachments
406 ) {
407 var actions = new ArrayList<HandleAction>();
408 if (message.getGroupContext().isPresent()) {
409 if (message.getGroupContext().get().getGroupV1().isPresent()) {
410 var groupInfo = message.getGroupContext().get().getGroupV1().get();
411 var groupId = GroupId.v1(groupInfo.getGroupId());
412 var group = groupHelper.getGroup(groupId);
413 if (group == null || group instanceof GroupInfoV1) {
414 var groupV1 = (GroupInfoV1) group;
415 switch (groupInfo.getType()) {
416 case UPDATE: {
417 if (groupV1 == null) {
418 groupV1 = new GroupInfoV1(groupId);
419 }
420
421 if (groupInfo.getAvatar().isPresent()) {
422 var avatar = groupInfo.getAvatar().get();
423 groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar);
424 }
425
426 if (groupInfo.getName().isPresent()) {
427 groupV1.name = groupInfo.getName().get();
428 }
429
430 if (groupInfo.getMembers().isPresent()) {
431 groupV1.addMembers(groupInfo.getMembers()
432 .get()
433 .stream()
434 .map(recipientResolver::resolveRecipient)
435 .collect(Collectors.toSet()));
436 }
437
438 account.getGroupStore().updateGroup(groupV1);
439 break;
440 }
441 case DELIVER:
442 if (groupV1 == null && !isSync) {
443 actions.add(new SendGroupInfoRequestAction(source, groupId));
444 }
445 break;
446 case QUIT: {
447 if (groupV1 != null) {
448 groupV1.removeMember(source);
449 account.getGroupStore().updateGroup(groupV1);
450 }
451 break;
452 }
453 case REQUEST_INFO:
454 if (groupV1 != null && !isSync) {
455 actions.add(new SendGroupInfoAction(source, groupV1.getGroupId()));
456 }
457 break;
458 }
459 } else {
460 // Received a group v1 message for a v2 group
461 }
462 }
463 if (message.getGroupContext().get().getGroupV2().isPresent()) {
464 final var groupContext = message.getGroupContext().get().getGroupV2().get();
465 final var groupMasterKey = groupContext.getMasterKey();
466
467 groupHelper.getOrMigrateGroup(groupMasterKey,
468 groupContext.getRevision(),
469 groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
470 }
471 }
472
473 final var conversationPartnerAddress = isSync ? destination : source;
474 if (conversationPartnerAddress != null && message.isEndSession()) {
475 account.getSessionStore().deleteAllSessions(conversationPartnerAddress);
476 }
477 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
478 if (message.getGroupContext().isPresent()) {
479 if (message.getGroupContext().get().getGroupV1().isPresent()) {
480 var groupInfo = message.getGroupContext().get().getGroupV1().get();
481 var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId()));
482 if (group != null) {
483 if (group.messageExpirationTime != message.getExpiresInSeconds()) {
484 group.messageExpirationTime = message.getExpiresInSeconds();
485 account.getGroupStore().updateGroup(group);
486 }
487 }
488 } else if (message.getGroupContext().get().getGroupV2().isPresent()) {
489 // disappearing message timer already stored in the DecryptedGroup
490 }
491 } else if (conversationPartnerAddress != null) {
492 contactHelper.setExpirationTimer(conversationPartnerAddress, message.getExpiresInSeconds());
493 }
494 }
495 if (!ignoreAttachments) {
496 if (message.getAttachments().isPresent()) {
497 for (var attachment : message.getAttachments().get()) {
498 attachmentHelper.downloadAttachment(attachment);
499 }
500 }
501 if (message.getSharedContacts().isPresent()) {
502 for (var contact : message.getSharedContacts().get()) {
503 if (contact.getAvatar().isPresent()) {
504 attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment());
505 }
506 }
507 }
508 if (message.getPreviews().isPresent()) {
509 final var previews = message.getPreviews().get();
510 for (var preview : previews) {
511 if (preview.getImage().isPresent()) {
512 attachmentHelper.downloadAttachment(preview.getImage().get());
513 }
514 }
515 }
516 if (message.getQuote().isPresent()) {
517 final var quote = message.getQuote().get();
518
519 for (var quotedAttachment : quote.getAttachments()) {
520 final var thumbnail = quotedAttachment.getThumbnail();
521 if (thumbnail != null) {
522 attachmentHelper.downloadAttachment(thumbnail);
523 }
524 }
525 }
526 }
527 if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
528 final ProfileKey profileKey;
529 try {
530 profileKey = new ProfileKey(message.getProfileKey().get());
531 } catch (InvalidInputException e) {
532 throw new AssertionError(e);
533 }
534 if (account.getSelfRecipientId().equals(source)) {
535 this.account.setProfileKey(profileKey);
536 }
537 this.account.getProfileStore().storeProfileKey(source, profileKey);
538 }
539 if (message.getSticker().isPresent()) {
540 final var messageSticker = message.getSticker().get();
541 final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId());
542 var sticker = account.getStickerStore().getSticker(stickerPackId);
543 if (sticker == null) {
544 sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
545 account.getStickerStore().updateSticker(sticker);
546 }
547 jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
548 }
549 return actions;
550 }
551 }