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