]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
Update libsignal-service-java
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / IncomingMessageHandler.java
index 251dfde38c3b2cefbe2eb38c0d9777bfade28aa2..dd9cb38fa5e9dd431b3216ffc699d87814c8fb85 100644 (file)
@@ -1,16 +1,14 @@
 package org.asamk.signal.manager.helper;
 
 import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.SignalDependencies;
-import org.asamk.signal.manager.TrustLevel;
-import org.asamk.signal.manager.UntrustedIdentityException;
 import org.asamk.signal.manager.actions.HandleAction;
 import org.asamk.signal.manager.actions.RefreshPreKeysAction;
 import org.asamk.signal.manager.actions.RenewSessionAction;
+import org.asamk.signal.manager.actions.ResendMessageAction;
 import org.asamk.signal.manager.actions.RetrieveProfileAction;
-import org.asamk.signal.manager.actions.RetrieveStorageDataAction;
 import org.asamk.signal.manager.actions.SendGroupInfoAction;
 import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
+import org.asamk.signal.manager.actions.SendProfileKeyAction;
 import org.asamk.signal.manager.actions.SendReceiptAction;
 import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
 import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
@@ -18,44 +16,68 @@ import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
 import org.asamk.signal.manager.actions.SendSyncContactsAction;
 import org.asamk.signal.manager.actions.SendSyncGroupsAction;
 import org.asamk.signal.manager.actions.SendSyncKeysAction;
+import org.asamk.signal.manager.actions.SyncStorageDataAction;
+import org.asamk.signal.manager.actions.UpdateAccountAttributesAction;
+import org.asamk.signal.manager.api.GroupId;
+import org.asamk.signal.manager.api.GroupNotFoundException;
 import org.asamk.signal.manager.api.MessageEnvelope;
 import org.asamk.signal.manager.api.Pair;
+import org.asamk.signal.manager.api.ReceiveConfig;
 import org.asamk.signal.manager.api.StickerPackId;
-import org.asamk.signal.manager.groups.GroupId;
-import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.api.TrustLevel;
+import org.asamk.signal.manager.api.UntrustedIdentityException;
 import org.asamk.signal.manager.groups.GroupUtils;
+import org.asamk.signal.manager.internal.SignalDependencies;
 import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
-import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
-import org.asamk.signal.manager.storage.stickers.Sticker;
+import org.asamk.signal.manager.storage.stickers.StickerPack;
 import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
 import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
 import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
 import org.signal.libsignal.metadata.ProtocolNoSessionException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
-import org.signal.zkgroup.InvalidInputException;
-import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.libsignal.protocol.InvalidMessageException;
+import org.signal.libsignal.protocol.UsePqRatchet;
+import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
+import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
+import org.signal.libsignal.zkgroup.InvalidInputException;
+import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.signalservice.api.InvalidMessageStructureException;
+import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
+import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult;
+import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
+import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
+import org.whispersystems.signalservice.api.push.ServiceId;
+import org.whispersystems.signalservice.api.push.ServiceId.ACI;
+import org.whispersystems.signalservice.api.push.ServiceIdType;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.internal.push.Envelope;
+import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 public final class IncomingMessageHandler {
 
-    private final static Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class);
+    private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class);
 
     private final SignalAccount account;
     private final SignalDependencies dependencies;
@@ -69,7 +91,7 @@ public final class IncomingMessageHandler {
 
     public Pair<List<HandleAction>, Exception> handleRetryEnvelope(
             final SignalServiceEnvelope envelope,
-            final boolean ignoreAttachments,
+            final ReceiveConfig receiveConfig,
             final Manager.ReceiveMessageHandler handler
     ) {
         final List<HandleAction> actions = new ArrayList<>();
@@ -80,12 +102,20 @@ public final class IncomingMessageHandler {
         SignalServiceContent content = null;
         if (!envelope.isReceipt()) {
             account.getIdentityKeyStore().setRetryingDecryption(true);
+            final var destination = getDestination(envelope).serviceId();
             try {
-                content = dependencies.getCipher().decrypt(envelope);
+                final var cipherResult = dependencies.getCipher(destination == null
+                                || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
+                        .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
+                content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
+                if (content == null) {
+                    return new Pair<>(List.of(), null);
+                }
             } catch (ProtocolUntrustedIdentityException e) {
-                final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender());
-                final var exception = new UntrustedIdentityException(account.getRecipientStore()
-                        .resolveRecipientAddress(recipientId), e.getSenderDevice());
+                final var recipientId = account.getRecipientResolver().resolveRecipient(e.getSender());
+                final var exception = new UntrustedIdentityException(account.getRecipientAddressResolver()
+                        .resolveRecipientAddress(recipientId)
+                        .toApiRecipientAddress(), e.getSenderDevice());
                 return new Pair<>(List.of(), exception);
             } catch (Exception e) {
                 return new Pair<>(List.of(), e);
@@ -93,50 +123,60 @@ public final class IncomingMessageHandler {
                 account.getIdentityKeyStore().setRetryingDecryption(false);
             }
         }
-        actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null));
+        actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, null));
         return new Pair<>(actions, null);
     }
 
     public Pair<List<HandleAction>, Exception> handleEnvelope(
             final SignalServiceEnvelope envelope,
-            final boolean ignoreAttachments,
+            final ReceiveConfig receiveConfig,
             final Manager.ReceiveMessageHandler handler
     ) {
         final var actions = new ArrayList<HandleAction>();
-        if (envelope.hasSourceUuid()) {
-            // Store uuid if we don't have it already
-            // address/uuid in envelope is sent by server
-            account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress());
-        }
         SignalServiceContent content = null;
         Exception exception = null;
+        envelope.getSourceServiceId().map(ServiceId::parseOrNull)
+                // Store uuid if we don't have it already
+                // uuid in envelope is sent by server
+                .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
         if (!envelope.isReceipt()) {
+            final var destination = getDestination(envelope).serviceId();
             try {
-                content = dependencies.getCipher().decrypt(envelope);
+                final var cipherResult = dependencies.getCipher(destination == null
+                                || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
+                        .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
+                content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
+                if (content == null) {
+                    return new Pair<>(List.of(), null);
+                }
             } catch (ProtocolUntrustedIdentityException e) {
-                final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender());
+                final var recipientId = account.getRecipientResolver().resolveRecipient(e.getSender());
                 actions.add(new RetrieveProfileAction(recipientId));
-                exception = new UntrustedIdentityException(account.getRecipientStore()
-                        .resolveRecipientAddress(recipientId), e.getSenderDevice());
-            } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | ProtocolInvalidMessageException e) {
+                exception = new UntrustedIdentityException(account.getRecipientAddressResolver()
+                        .resolveRecipientAddress(recipientId)
+                        .toApiRecipientAddress(), e.getSenderDevice());
+            } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException |
+                     ProtocolInvalidMessageException e) {
                 logger.debug("Failed to decrypt incoming message", e);
-                final var sender = account.getRecipientStore().resolveRecipient(e.getSender());
+                if (e instanceof ProtocolInvalidKeyIdException) {
+                    actions.add(RefreshPreKeysAction.create());
+                }
+                final var sender = account.getRecipientResolver().resolveRecipient(e.getSender());
                 if (context.getContactHelper().isContactBlocked(sender)) {
                     logger.debug("Received invalid message from blocked contact, ignoring.");
                 } else {
-                    final var senderProfile = context.getProfileHelper().getRecipientProfile(sender);
-                    final var selfProfile = context.getProfileHelper()
-                            .getRecipientProfile(account.getSelfRecipientId());
-                    if ((!sender.equals(account.getSelfRecipientId()) || e.getSenderDevice() != account.getDeviceId())
-                            && senderProfile != null
-                            && senderProfile.getCapabilities().contains(Profile.Capability.senderKey)
-                            && selfProfile != null
-                            && selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
-                        logger.debug("Received invalid message, requesting message resend.");
-                        actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
-                    } else {
+                    var serviceId = ServiceId.parseOrNull(e.getSender());
+                    if (serviceId != null) {
+                        final var isSelf = sender.equals(account.getSelfRecipientId())
+                                && e.getSenderDevice() == account.getDeviceId();
                         logger.debug("Received invalid message, queuing renew session action.");
-                        actions.add(new RenewSessionAction(sender));
+                        actions.add(new RenewSessionAction(sender, serviceId, destination));
+                        if (!isSelf) {
+                            logger.debug("Received invalid message, requesting message resend.");
+                            actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
+                        }
+                    } else {
+                        logger.debug("Received invalid message from invalid sender: {}", e.getSender());
                     }
                 }
                 exception = e;
@@ -149,41 +189,96 @@ public final class IncomingMessageHandler {
             }
         }
 
-        actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, exception));
+        actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, exception));
         return new Pair<>(actions, exception);
     }
 
+    private SignalServiceContent validate(
+            Envelope envelope,
+            SignalServiceCipherResult cipherResult,
+            long serverDeliveredTimestamp
+    ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
+        final var content = cipherResult.getContent();
+        final var envelopeMetadata = cipherResult.getMetadata();
+        final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, content, account.getAci());
+
+        if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
+            logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
+            return null;
+        }
+
+        if (validationResult instanceof EnvelopeContentValidator.Result.UnsupportedDataMessage v) {
+            logger.warn("Unsupported DataMessage! Our version: {}, their version: {}",
+                    v.getOurVersion(),
+                    v.getTheirVersion());
+            return null;
+        }
+
+        return SignalServiceContent.Companion.createFrom(account.getNumber(),
+                envelope,
+                envelopeMetadata,
+                content,
+                serverDeliveredTimestamp);
+    }
+
     private List<HandleAction> checkAndHandleMessage(
             final SignalServiceEnvelope envelope,
             final SignalServiceContent content,
-            final boolean ignoreAttachments,
+            final ReceiveConfig receiveConfig,
             final Manager.ReceiveMessageHandler handler,
             final Exception exception
     ) {
-        if (!envelope.hasSourceUuid() && content != null) {
+        if (content != null) {
             // Store uuid if we don't have it already
             // address/uuid is validated by unidentified sender certificate
-            account.getRecipientStore().resolveRecipientTrusted(content.getSender());
+
+            boolean handledPniSignature = false;
+            if (content.getPniSignatureMessage().isPresent()) {
+                final var message = content.getPniSignatureMessage().get();
+                final var senderAddress = getSenderAddress(envelope, content);
+                if (senderAddress != null) {
+                    handledPniSignature = handlePniSignatureMessage(message, senderAddress);
+                }
+            }
+            if (!handledPniSignature) {
+                account.getRecipientTrustedResolver().resolveRecipientTrusted(content.getSender());
+            }
+        }
+        if (envelope.isReceipt()) {
+            final var senderDeviceAddress = getSender(envelope, content);
+            final var sender = senderDeviceAddress.serviceId();
+            final var senderDeviceId = senderDeviceAddress.deviceId();
+            account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId);
         }
+
+        var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content);
+        final var groupContext = getGroupContext(content);
+        if (groupContext != null && groupContext.getGroupV2().isPresent()) {
+            handleGroupV2Context(groupContext.getGroupV2().get());
+        }
+        // Check again in case the user just joined the group
+        notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content);
+
         if (isMessageBlocked(envelope, content)) {
             logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
             return List.of();
-        } else if (isNotAllowedToSendToGroup(envelope, content)) {
+        } else if (notAllowedToSendToGroup) {
+            final var senderAddress = getSenderAddress(envelope, content);
             logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
-                    (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(),
+                    senderAddress == null ? null : senderAddress.getIdentifier(),
                     envelope.getTimestamp());
             return List.of();
         } else {
             List<HandleAction> actions;
             if (content != null) {
-                actions = handleMessage(envelope, content, ignoreAttachments);
+                actions = handleMessage(envelope, content, receiveConfig);
             } else {
                 actions = List.of();
             }
             handler.handleMessage(MessageEnvelope.from(envelope,
                     content,
-                    account.getRecipientStore(),
-                    account.getRecipientStore()::resolveRecipientAddress,
+                    account.getRecipientResolver(),
+                    account.getRecipientAddressResolver(),
                     context.getAttachmentHelper()::getAttachmentFile,
                     exception), exception);
             return actions;
@@ -191,76 +286,220 @@ public final class IncomingMessageHandler {
     }
 
     public List<HandleAction> handleMessage(
-            SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments
+            SignalServiceEnvelope envelope,
+            SignalServiceContent content,
+            ReceiveConfig receiveConfig
     ) {
         var actions = new ArrayList<HandleAction>();
-        final var senderPair = getSender(envelope, content);
-        final var sender = senderPair.first();
-        final var senderDeviceId = senderPair.second();
+        final var senderDeviceAddress = getSender(envelope, content);
+        final var sender = senderDeviceAddress.recipientId();
+        final var senderServiceId = senderDeviceAddress.serviceId();
+        final var senderDeviceId = senderDeviceAddress.deviceId();
+        final var destination = getDestination(envelope);
+
+        if (account.getPni().equals(destination.serviceId)) {
+            account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
+        } else if (account.getAci().equals(destination.serviceId)) {
+            account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false);
+        }
+
+        if (content.getReceiptMessage().isPresent()) {
+            final var message = content.getReceiptMessage().get();
+            if (message.isDeliveryReceipt()) {
+                account.getMessageSendLogStore()
+                        .deleteEntriesForRecipient(message.getTimestamps(), senderServiceId, senderDeviceId);
+            }
+        }
 
         if (content.getSenderKeyDistributionMessage().isPresent()) {
             final var message = content.getSenderKeyDistributionMessage().get();
-            final var protocolAddress = new SignalProtocolAddress(context.getRecipientHelper()
-                    .resolveSignalServiceAddress(sender)
-                    .getIdentifier(), senderDeviceId);
+            final var protocolAddress = senderServiceId.toProtocolAddress(senderDeviceId);
             logger.debug("Received a sender key distribution message for distributionId {} from {}",
                     message.getDistributionId(),
                     protocolAddress);
-            dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message);
+            new SignalGroupSessionBuilder(dependencies.getSessionLock(),
+                    new GroupSessionBuilder(account.getSenderKeyStore())).process(protocolAddress, message);
         }
 
         if (content.getDecryptionErrorMessage().isPresent()) {
             var message = content.getDecryptionErrorMessage().get();
-            logger.debug("Received a decryption error message (resend request for {})", message.getTimestamp());
-            if (message.getRatchetKey().isPresent()) {
-                if (message.getDeviceId() == account.getDeviceId() && account.getSessionStore()
-                        .isCurrentRatchetKey(sender, senderDeviceId, message.getRatchetKey().get())) {
-                    logger.debug("Renewing the session with sender");
-                    actions.add(new RenewSessionAction(sender));
-                }
+            logger.debug("Received a decryption error message from {}.{} (resend request for {})",
+                    sender,
+                    senderDeviceId,
+                    message.getTimestamp());
+            if (message.getDeviceId() == account.getDeviceId()) {
+                handleDecryptionErrorMessage(actions,
+                        sender,
+                        senderServiceId,
+                        senderDeviceId,
+                        message,
+                        destination.serviceId());
             } else {
-                logger.debug("Reset shared sender keys with this recipient");
-                account.getSenderKeyStore().deleteSharedWith(sender);
+                logger.debug("Request is for another one of our devices");
             }
         }
 
-        if (content.getDataMessage().isPresent()) {
-            var message = content.getDataMessage().get();
+        if (content.getDataMessage().isPresent() || content.getEditMessage().isPresent()) {
+            var message = content.getDataMessage().isPresent()
+                    ? content.getDataMessage().get()
+                    : content.getEditMessage().get().getDataMessage();
 
             if (content.isNeedsReceipt()) {
-                actions.add(new SendReceiptAction(sender, message.getTimestamp()));
+                actions.add(new SendReceiptAction(sender,
+                        SignalServiceReceiptMessage.Type.DELIVERY,
+                        message.getTimestamp()));
+            } else {
+                // Message wasn't sent as unidentified sender message
+                final var contact = context.getAccount().getContactStore().getContact(sender);
+                if (account.isPrimaryDevice()
+                        && contact != null
+                        && !contact.isBlocked()
+                        && contact.isProfileSharingEnabled()) {
+                    actions.add(UpdateAccountAttributesAction.create());
+                    actions.add(new SendProfileKeyAction(sender));
+                }
+            }
+            if (receiveConfig.sendReadReceipts()) {
+                actions.add(new SendReceiptAction(sender,
+                        SignalServiceReceiptMessage.Type.READ,
+                        message.getTimestamp()));
             }
 
             actions.addAll(handleSignalServiceDataMessage(message,
                     false,
-                    sender,
-                    account.getSelfRecipientId(),
-                    ignoreAttachments));
+                    senderDeviceAddress,
+                    destination,
+                    receiveConfig.ignoreAttachments()));
+        }
+
+        if (content.getStoryMessage().isPresent()) {
+            final var message = content.getStoryMessage().get();
+            actions.addAll(handleSignalServiceStoryMessage(message, sender, receiveConfig.ignoreAttachments()));
         }
 
         if (content.getSyncMessage().isPresent()) {
             var syncMessage = content.getSyncMessage().get();
-            actions.addAll(handleSyncMessage(syncMessage, sender, ignoreAttachments));
+            actions.addAll(handleSyncMessage(envelope,
+                    syncMessage,
+                    senderDeviceAddress,
+                    receiveConfig.ignoreAttachments()));
         }
 
         return actions;
     }
 
+    private boolean handlePniSignatureMessage(
+            final SignalServicePniSignatureMessage message,
+            final SignalServiceAddress senderAddress
+    ) {
+        final var aci = senderAddress.getServiceId();
+        final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci);
+        final var pni = message.getPni();
+        final var pniIdentity = account.getIdentityKeyStore().getIdentityInfo(pni);
+
+        if (aciIdentity == null || pniIdentity == null || aci.equals(pni)) {
+            return false;
+        }
+
+        final var verified = pniIdentity.getIdentityKey()
+                .verifyAlternateIdentity(aciIdentity.getIdentityKey(), message.getSignature());
+
+        if (!verified) {
+            logger.debug("Invalid PNI signature of ACI {} with PNI {}", aci, pni);
+            return false;
+        }
+
+        logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
+        account.getRecipientTrustedResolver()
+                .resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
+                        Optional.of(pni),
+                        senderAddress.getNumber());
+        return true;
+    }
+
+    private void handleDecryptionErrorMessage(
+            final List<HandleAction> actions,
+            final RecipientId sender,
+            final ServiceId senderServiceId,
+            final int senderDeviceId,
+            final DecryptionErrorMessage message,
+            final ServiceId destination
+    ) {
+        final var logEntries = account.getMessageSendLogStore()
+                .findMessages(senderServiceId,
+                        senderDeviceId,
+                        message.getTimestamp(),
+                        message.getRatchetKey().isEmpty());
+
+        for (final var logEntry : logEntries) {
+            actions.add(new ResendMessageAction(sender, message.getTimestamp(), logEntry));
+        }
+
+        if (message.getRatchetKey().isPresent()) {
+            final var sessionStore = account.getAccountData(destination).getSessionStore();
+            if (sessionStore.isCurrentRatchetKey(senderServiceId, senderDeviceId, message.getRatchetKey().get())) {
+                if (logEntries.isEmpty()) {
+                    logger.debug("Renewing the session with sender");
+                    actions.add(new RenewSessionAction(sender, senderServiceId, destination));
+                } else {
+                    logger.trace("Archiving the session with sender, a resend message has already been queued");
+                    sessionStore.archiveSessions(senderServiceId);
+                }
+            }
+            return;
+        }
+
+        var found = false;
+        for (final var logEntry : logEntries) {
+            if (logEntry.groupId().isEmpty()) {
+                continue;
+            }
+            final var group = account.getGroupStore().getGroup(logEntry.groupId().get());
+            if (group == null) {
+                continue;
+            }
+            found = true;
+            logger.trace("Deleting shared sender key with {} ({}): {}",
+                    sender,
+                    senderDeviceId,
+                    group.getDistributionId());
+            account.getSenderKeyStore().deleteSharedWith(senderServiceId, senderDeviceId, group.getDistributionId());
+        }
+        if (!found) {
+            logger.debug("Reset all shared sender keys with this recipient, no related message found in send log");
+            account.getSenderKeyStore().deleteSharedWith(senderServiceId);
+        }
+    }
+
     private List<HandleAction> handleSyncMessage(
-            final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments
+            final SignalServiceEnvelope envelope,
+            final SignalServiceSyncMessage syncMessage,
+            final DeviceAddress sender,
+            final boolean ignoreAttachments
     ) {
         var actions = new ArrayList<HandleAction>();
         account.setMultiDevice(true);
         if (syncMessage.getSent().isPresent()) {
             var message = syncMessage.getSent().get();
-            final var destination = message.getDestination().orNull();
-            actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
-                    true,
-                    sender,
-                    destination == null ? null : context.getRecipientHelper().resolveRecipient(destination),
-                    ignoreAttachments));
-        }
-        if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) {
+            final var destination = message.getDestination().orElse(null);
+            if (message.getDataMessage().isPresent()) {
+                actions.addAll(handleSignalServiceDataMessage(message.getDataMessage().get(),
+                        true,
+                        sender,
+                        destination == null
+                                ? null
+                                : new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
+                                        destination.getServiceId(),
+                                        0),
+                        ignoreAttachments));
+            }
+            if (message.getStoryMessage().isPresent()) {
+                actions.addAll(handleSignalServiceStoryMessage(message.getStoryMessage().get(),
+                        sender.recipientId(),
+                        ignoreAttachments));
+            }
+        }
+        if (syncMessage.getRequest().isPresent() && account.isPrimaryDevice()) {
             var rm = syncMessage.getRequest().get();
             if (rm.isContactsRequest()) {
                 actions.add(SendSyncContactsAction.create());
@@ -277,18 +516,25 @@ public final class IncomingMessageHandler {
             if (rm.isConfigurationRequest()) {
                 actions.add(SendSyncConfigurationAction.create());
             }
+            actions.add(SyncStorageDataAction.create());
         }
         if (syncMessage.getGroups().isPresent()) {
-            logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
+            try {
+                final var groupsMessage = syncMessage.getGroups().get();
+                context.getAttachmentHelper()
+                        .retrieveAttachment(groupsMessage, context.getSyncHelper()::handleSyncDeviceGroups);
+            } catch (Exception e) {
+                logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage());
+            }
         }
         if (syncMessage.getBlockedList().isPresent()) {
             final var blockedListMessage = syncMessage.getBlockedList().get();
-            for (var address : blockedListMessage.getAddresses()) {
-                context.getContactHelper()
-                        .setContactBlocked(context.getRecipientHelper().resolveRecipient(address), true);
+            for (var individual : blockedListMessage.individuals) {
+                final var address = new RecipientAddress(individual.getAci(), individual.getE164());
+                final var recipientId = account.getRecipientResolver().resolveRecipient(address);
+                context.getContactHelper().setContactBlocked(recipientId, true);
             }
-            for (var groupId : blockedListMessage.getGroupIds()
-                    .stream()
+            for (var groupId : blockedListMessage.groupIds.stream()
                     .map(GroupId::unknownVersion)
                     .collect(Collectors.toSet())) {
                 try {
@@ -312,52 +558,54 @@ public final class IncomingMessageHandler {
         if (syncMessage.getVerified().isPresent()) {
             final var verifiedMessage = syncMessage.getVerified().get();
             account.getIdentityKeyStore()
-                    .setIdentityTrustLevel(account.getRecipientStore()
-                                    .resolveRecipientTrusted(verifiedMessage.getDestination()),
+                    .setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
                             verifiedMessage.getIdentityKey(),
                             TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
         }
         if (syncMessage.getStickerPackOperations().isPresent()) {
             final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
             for (var m : stickerPackOperationMessages) {
-                if (!m.getPackId().isPresent()) {
+                if (m.getPackId().isEmpty()) {
                     continue;
                 }
                 final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
-                final var installed = !m.getType().isPresent()
+                final var stickerPackKey = m.getPackKey().orElse(null);
+                final var installed = m.getType().isEmpty()
                         || m.getType().get() == StickerPackOperationMessage.Type.INSTALL;
 
-                var sticker = account.getStickerStore().getStickerPack(stickerPackId);
-                if (m.getPackKey().isPresent()) {
-                    if (sticker == null) {
-                        sticker = new Sticker(stickerPackId, m.getPackKey().get());
-                    }
-                    if (installed) {
-                        context.getJobExecutor()
-                                .enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get()));
-                    }
-                }
+                final var sticker = context.getStickerHelper()
+                        .addOrUpdateStickerPack(stickerPackId, stickerPackKey, installed);
 
-                if (sticker != null) {
-                    sticker.setInstalled(installed);
-                    account.getStickerStore().updateSticker(sticker);
+                if (sticker != null && installed) {
+                    context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, sticker.packKey()));
                 }
             }
         }
         if (syncMessage.getFetchType().isPresent()) {
             switch (syncMessage.getFetchType().get()) {
-                case LOCAL_PROFILE:
-                    actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
-                case STORAGE_MANIFEST:
-                    actions.add(RetrieveStorageDataAction.create());
+                case LOCAL_PROFILE -> actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
+                case STORAGE_MANIFEST -> actions.add(SyncStorageDataAction.create());
             }
         }
         if (syncMessage.getKeys().isPresent()) {
             final var keysMessage = syncMessage.getKeys().get();
-            if (keysMessage.getStorageService().isPresent()) {
-                final var storageKey = keysMessage.getStorageService().get();
+            if (keysMessage.getAccountEntropyPool() != null) {
+                final var aep = keysMessage.getAccountEntropyPool();
+                account.setAccountEntropyPool(aep);
+                actions.add(SyncStorageDataAction.create());
+            } else if (keysMessage.getMaster() != null) {
+                final var masterKey = keysMessage.getMaster();
+                account.setMasterKey(masterKey);
+                actions.add(SyncStorageDataAction.create());
+            } else if (keysMessage.getStorageService() != null) {
+                final var storageKey = keysMessage.getStorageService();
                 account.setStorageKey(storageKey);
-                actions.add(RetrieveStorageDataAction.create());
+                actions.add(SyncStorageDataAction.create());
+            }
+            if (keysMessage.getMediaRootBackupKey() != null) {
+                final var mrb = keysMessage.getMediaRootBackupKey();
+                account.setMediaRootBackupKey(mrb);
+                actions.add(SyncStorageDataAction.create());
             }
         }
         if (syncMessage.getConfiguration().isPresent()) {
@@ -377,76 +625,101 @@ public final class IncomingMessageHandler {
                         .get());
             }
         }
+        if (syncMessage.getPniChangeNumber().isPresent()) {
+            final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
+            logger.debug("Received PNI change number sync message, applying.");
+            final var updatedPniString = envelope.getUpdatedPni();
+            if (updatedPniString != null && !updatedPniString.isEmpty()) {
+                final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
+                context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
+            }
+        }
         return actions;
     }
 
+    private SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
+        if (content == null) {
+            return null;
+        }
+
+        if (content.getDataMessage().isPresent()) {
+            var message = content.getDataMessage().get();
+            if (message.getGroupContext().isPresent()) {
+                return message.getGroupContext().get();
+            }
+        }
+
+        if (content.getStoryMessage().isPresent()) {
+            var message = content.getStoryMessage().get();
+            if (message.getGroupContext().isPresent()) {
+                try {
+                    return SignalServiceGroupContext.create(null, message.getGroupContext().get());
+                } catch (InvalidMessageException e) {
+                    throw new AssertionError(e);
+                }
+            }
+        }
+
+        return null;
+    }
+
     private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
-        SignalServiceAddress source;
-        if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
-            source = envelope.getSourceAddress();
-        } else if (content != null) {
-            source = content.getSender();
-        } else {
+        SignalServiceAddress source = getSenderAddress(envelope, content);
+        if (source == null) {
             return false;
         }
-        final var recipientId = context.getRecipientHelper().resolveRecipient(source);
+        final var recipientId = account.getRecipientResolver().resolveRecipient(source);
         if (context.getContactHelper().isContactBlocked(recipientId)) {
             return true;
         }
 
-        if (content != null && content.getDataMessage().isPresent()) {
-            var message = content.getDataMessage().get();
-            if (message.getGroupContext().isPresent()) {
-                var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
-                return context.getGroupHelper().isGroupBlocked(groupId);
-            }
+        final var groupContext = getGroupContext(content);
+        if (groupContext != null) {
+            var groupId = GroupUtils.getGroupId(groupContext);
+            return context.getGroupHelper().isGroupBlocked(groupId);
         }
 
         return false;
     }
 
     private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) {
-        SignalServiceAddress source;
-        if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
-            source = envelope.getSourceAddress();
-        } else if (content != null) {
-            source = content.getSender();
-        } else {
+        SignalServiceAddress source = getSenderAddress(envelope, content);
+        if (source == null) {
             return false;
         }
 
-        if (content == null || !content.getDataMessage().isPresent()) {
+        final var groupContext = getGroupContext(content);
+        if (groupContext == null) {
             return false;
         }
 
-        var message = content.getDataMessage().get();
-        if (!message.getGroupContext().isPresent()) {
-            return false;
-        }
-
-        if (message.getGroupContext().get().getGroupV1().isPresent()) {
-            var groupInfo = message.getGroupContext().get().getGroupV1().get();
+        if (groupContext.getGroupV1().isPresent()) {
+            var groupInfo = groupContext.getGroupV1().get();
             if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
                 return false;
             }
         }
 
-        var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
+        var groupId = GroupUtils.getGroupId(groupContext);
         var group = context.getGroupHelper().getGroup(groupId);
         if (group == null) {
             return false;
         }
 
-        final var recipientId = context.getRecipientHelper().resolveRecipient(source);
-        if (!group.isMember(recipientId) && !(group.isPendingMember(recipientId) && message.isGroupV2Update())) {
+        final var message = content.getDataMessage().orElse(null);
+
+        final var recipientId = account.getRecipientResolver().resolveRecipient(source);
+        if (!group.isMember(recipientId) && !(
+                group.isPendingMember(recipientId) && message != null && message.isGroupV2Update()
+        )) {
             return true;
         }
 
         if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
-            return message.getBody().isPresent()
+            return message == null
+                    || message.getBody().isPresent()
                     || message.getAttachments().isPresent()
-                    || message.getQuote()
-                    .isPresent()
+                    || message.getQuote().isPresent()
                     || message.getPreviews().isPresent()
                     || message.getMentions().isPresent()
                     || message.getSticker().isPresent();
@@ -457,20 +730,21 @@ public final class IncomingMessageHandler {
     private List<HandleAction> handleSignalServiceDataMessage(
             SignalServiceDataMessage message,
             boolean isSync,
-            RecipientId source,
-            RecipientId destination,
+            DeviceAddress source,
+            DeviceAddress destination,
             boolean ignoreAttachments
     ) {
         var actions = new ArrayList<HandleAction>();
         if (message.getGroupContext().isPresent()) {
-            if (message.getGroupContext().get().getGroupV1().isPresent()) {
-                var groupInfo = message.getGroupContext().get().getGroupV1().get();
+            final var groupContext = message.getGroupContext().get();
+            if (groupContext.getGroupV1().isPresent()) {
+                var groupInfo = groupContext.getGroupV1().get();
                 var groupId = GroupId.v1(groupInfo.getGroupId());
                 var group = context.getGroupHelper().getGroup(groupId);
                 if (group == null || group instanceof GroupInfoV1) {
                     var groupV1 = (GroupInfoV1) group;
                     switch (groupInfo.getType()) {
-                        case UPDATE: {
+                        case UPDATE -> {
                             if (groupV1 == null) {
                                 groupV1 = new GroupInfoV1(groupId);
                             }
@@ -485,57 +759,54 @@ public final class IncomingMessageHandler {
                             }
 
                             if (groupInfo.getMembers().isPresent()) {
+                                final var recipientResolver = account.getRecipientResolver();
                                 groupV1.addMembers(groupInfo.getMembers()
                                         .get()
                                         .stream()
-                                        .map(context.getRecipientHelper()::resolveRecipient)
+                                        .map(recipientResolver::resolveRecipient)
                                         .collect(Collectors.toSet()));
                             }
 
                             account.getGroupStore().updateGroup(groupV1);
-                            break;
                         }
-                        case DELIVER:
+                        case DELIVER -> {
                             if (groupV1 == null && !isSync) {
-                                actions.add(new SendGroupInfoRequestAction(source, groupId));
+                                actions.add(new SendGroupInfoRequestAction(source.recipientId(), groupId));
                             }
-                            break;
-                        case QUIT: {
+                        }
+                        case QUIT -> {
                             if (groupV1 != null) {
-                                groupV1.removeMember(source);
+                                groupV1.removeMember(source.recipientId());
                                 account.getGroupStore().updateGroup(groupV1);
                             }
-                            break;
                         }
-                        case REQUEST_INFO:
+                        case REQUEST_INFO -> {
                             if (groupV1 != null && !isSync) {
-                                actions.add(new SendGroupInfoAction(source, groupV1.getGroupId()));
+                                actions.add(new SendGroupInfoAction(source.recipientId(), groupV1.getGroupId()));
                             }
-                            break;
+                        }
                     }
                 } else {
                     // Received a group v1 message for a v2 group
                 }
             }
-            if (message.getGroupContext().get().getGroupV2().isPresent()) {
-                final var groupContext = message.getGroupContext().get().getGroupV2().get();
-                final var groupMasterKey = groupContext.getMasterKey();
-
-                context.getGroupHelper()
-                        .getOrMigrateGroup(groupMasterKey,
-                                groupContext.getRevision(),
-                                groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
+            if (groupContext.getGroupV2().isPresent()) {
+                handleGroupV2Context(groupContext.getGroupV2().get());
             }
         }
 
+        final var selfAddress = isSync ? source : destination;
         final var conversationPartnerAddress = isSync ? destination : source;
         if (conversationPartnerAddress != null && message.isEndSession()) {
-            account.getSessionStore().deleteAllSessions(conversationPartnerAddress);
+            account.getAccountData(selfAddress.serviceId())
+                    .getSessionStore()
+                    .deleteAllSessions(conversationPartnerAddress.serviceId());
         }
         if (message.isExpirationUpdate() || message.getBody().isPresent()) {
             if (message.getGroupContext().isPresent()) {
-                if (message.getGroupContext().get().getGroupV1().isPresent()) {
-                    var groupInfo = message.getGroupContext().get().getGroupV1().get();
+                final var groupContext = message.getGroupContext().get();
+                if (groupContext.getGroupV1().isPresent()) {
+                    var groupInfo = groupContext.getGroupV1().get();
                     var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId()));
                     if (group != null) {
                         if (group.messageExpirationTime != message.getExpiresInSeconds()) {
@@ -543,12 +814,14 @@ public final class IncomingMessageHandler {
                             account.getGroupStore().updateGroup(group);
                         }
                     }
-                } else if (message.getGroupContext().get().getGroupV2().isPresent()) {
+                } else if (groupContext.getGroupV2().isPresent()) {
                     // disappearing message timer already stored in the DecryptedGroup
                 }
             } else if (conversationPartnerAddress != null) {
                 context.getContactHelper()
-                        .setExpirationTimer(conversationPartnerAddress, message.getExpiresInSeconds());
+                        .setExpirationTimer(conversationPartnerAddress.recipientId(),
+                                message.getExpiresInSeconds(),
+                                message.getExpireTimerVersion());
             }
         }
         if (!ignoreAttachments) {
@@ -575,46 +848,130 @@ public final class IncomingMessageHandler {
             if (message.getQuote().isPresent()) {
                 final var quote = message.getQuote().get();
 
-                for (var quotedAttachment : quote.getAttachments()) {
-                    final var thumbnail = quotedAttachment.getThumbnail();
-                    if (thumbnail != null) {
-                        context.getAttachmentHelper().downloadAttachment(thumbnail);
+                if (quote.getAttachments() != null) {
+                    for (var quotedAttachment : quote.getAttachments()) {
+                        final var thumbnail = quotedAttachment.getThumbnail();
+                        if (thumbnail != null) {
+                            context.getAttachmentHelper().downloadAttachment(thumbnail);
+                        }
                     }
                 }
             }
         }
-        if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
-            final ProfileKey profileKey;
-            try {
-                profileKey = new ProfileKey(message.getProfileKey().get());
-            } catch (InvalidInputException e) {
-                throw new AssertionError(e);
-            }
-            if (account.getSelfRecipientId().equals(source)) {
-                this.account.setProfileKey(profileKey);
-            }
-            this.account.getProfileStore().storeProfileKey(source, profileKey);
+        if (message.getGiftBadge().isPresent()) {
+            handleIncomingGiftBadge(message.getGiftBadge().get());
+        }
+        if (message.getProfileKey().isPresent()) {
+            handleIncomingProfileKey(message.getProfileKey().get(), source.recipientId());
         }
         if (message.getSticker().isPresent()) {
             final var messageSticker = message.getSticker().get();
             final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId());
             var sticker = account.getStickerStore().getStickerPack(stickerPackId);
             if (sticker == null) {
-                sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
-                account.getStickerStore().updateSticker(sticker);
+                sticker = new StickerPack(stickerPackId, messageSticker.getPackKey());
+                account.getStickerStore().addStickerPack(sticker);
             }
             context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
         }
         return actions;
     }
 
-    private Pair<RecipientId, Integer> getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
-        if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
-            return new Pair<>(context.getRecipientHelper().resolveRecipient(envelope.getSourceAddress()),
+    private void handleIncomingGiftBadge(final SignalServiceDataMessage.GiftBadge giftBadge) {
+        // TODO
+    }
+
+    private List<HandleAction> handleSignalServiceStoryMessage(
+            SignalServiceStoryMessage message,
+            RecipientId source,
+            boolean ignoreAttachments
+    ) {
+        var actions = new ArrayList<HandleAction>();
+        if (message.getGroupContext().isPresent()) {
+            handleGroupV2Context(message.getGroupContext().get());
+        }
+
+        if (!ignoreAttachments) {
+            if (message.getFileAttachment().isPresent()) {
+                context.getAttachmentHelper().downloadAttachment(message.getFileAttachment().get());
+            }
+            if (message.getTextAttachment().isPresent()) {
+                final var textAttachment = message.getTextAttachment().get();
+                if (textAttachment.getPreview().isPresent()) {
+                    final var preview = textAttachment.getPreview().get();
+                    if (preview.getImage().isPresent()) {
+                        context.getAttachmentHelper().downloadAttachment(preview.getImage().get());
+                    }
+                }
+            }
+        }
+
+        if (message.getProfileKey().isPresent()) {
+            handleIncomingProfileKey(message.getProfileKey().get(), source);
+        }
+
+        return actions;
+    }
+
+    private void handleGroupV2Context(final SignalServiceGroupV2 groupContext) {
+        final var groupMasterKey = groupContext.getMasterKey();
+
+        context.getGroupHelper()
+                .getOrMigrateGroup(groupMasterKey,
+                        groupContext.getRevision(),
+                        groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
+    }
+
+    private void handleIncomingProfileKey(final byte[] profileKeyBytes, final RecipientId source) {
+        if (profileKeyBytes.length != 32) {
+            logger.debug("Received invalid profile key of length {}", profileKeyBytes.length);
+            return;
+        }
+        final ProfileKey profileKey;
+        try {
+            profileKey = new ProfileKey(profileKeyBytes);
+        } catch (InvalidInputException e) {
+            throw new AssertionError(e);
+        }
+        if (account.getSelfRecipientId().equals(source)) {
+            this.account.setProfileKey(profileKey);
+        }
+        this.account.getProfileStore().storeProfileKey(source, profileKey);
+    }
+
+    private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
+        final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
+        if (!envelope.isUnidentifiedSender() && serviceId != null) {
+            return new SignalServiceAddress(serviceId);
+        } else if (content != null) {
+            return content.getSender();
+        } else {
+            return null;
+        }
+    }
+
+    private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
+        final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
+        if (!envelope.isUnidentifiedSender() && serviceId != null) {
+            return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
+                    serviceId,
                     envelope.getSourceDevice());
         } else {
-            return new Pair<>(context.getRecipientHelper().resolveRecipient(content.getSender()),
+            return new DeviceAddress(account.getRecipientResolver().resolveRecipient(content.getSender()),
+                    content.getSender().getServiceId(),
                     content.getSenderDevice());
         }
     }
+
+    private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
+        final var destination = envelope.getDestinationServiceId();
+        if (destination == null || destination.isUnknown()) {
+            return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
+        }
+        return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
+                destination,
+                account.getDeviceId());
+    }
+
+    private record DeviceAddress(RecipientId recipientId, ServiceId serviceId, int deviceId) {}
 }