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