From 2c88945e6b686acc71fd2ec50c7342f46967a495 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 4 Jun 2026 00:14:50 -0400 Subject: [PATCH] Fix sending messages to self when your session is deleted. --- .../api/SignalServiceMessageSender.java | 20 ++++++++++--------- .../signal/network/service/MessageService.kt | 17 +++++++++------- .../network/service/MessageServiceTest.kt | 17 ++++++++++++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 602e5ea811..2cca10b439 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -2808,22 +2808,24 @@ public class SignalServiceMessageSender { boolean story) throws IOException, InvalidKeyException, UntrustedIdentityException { - List messages = new LinkedList<>(); + List messages = new LinkedList<>(); + List subDevices = aciStore.getSubDeviceSessions(recipient.getIdentifier()); + Set deviceIds = subDevices.stream() + .filter((id) -> aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), id))) + .collect(Collectors.toSet()); - List subDevices = aciStore.getSubDeviceSessions(recipient.getIdentifier()); - - List deviceIds = new ArrayList<>(subDevices.size() + 1); deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID); - deviceIds.addAll(subDevices); if (recipient.matches(localAddress)) { - deviceIds.remove(Integer.valueOf(localDeviceId)); + deviceIds.remove(localDeviceId); + + if (deviceIds.isEmpty()) { + deviceIds.add(localDeviceId); + } } for (int deviceId : deviceIds) { - if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { - messages.add(getEncryptedMessage(recipient, sealedSenderAccess, deviceId, plaintext, story)); - } + messages.add(getEncryptedMessage(recipient, sealedSenderAccess, deviceId, plaintext, story)); } return new OutgoingPushMessageList(recipient.getIdentifier(), timestamp, messages, online, urgent); diff --git a/lib/network/src/main/java/org/signal/network/service/MessageService.kt b/lib/network/src/main/java/org/signal/network/service/MessageService.kt index 91fc73bbfd..7e695c106e 100644 --- a/lib/network/src/main/java/org/signal/network/service/MessageService.kt +++ b/lib/network/src/main/java/org/signal/network/service/MessageService.kt @@ -396,17 +396,20 @@ open class MessageService( } private fun targetDeviceIds(serviceId: ServiceId): List { - val subDevices: MutableSet = (protocolStore.getSubDeviceSessions(serviceId.toString()) + SignalServiceAddress.DEFAULT_DEVICE_ID).toMutableSet() + val devices: MutableSet = protocolStore.getSubDeviceSessions(serviceId.toString()) + .filter { protocolStore.containsSession(SignalProtocolAddress(serviceId.libSignalServiceId, it)) } + .toMutableSet() + + devices += SignalServiceAddress.DEFAULT_DEVICE_ID - // When sending to self, skip our own device. if (serviceId == localAddress.serviceId) { - subDevices -= localDeviceId + devices -= localDeviceId + if (devices.isEmpty()) { + devices += localDeviceId + } } - return subDevices - .filter { it == SignalServiceAddress.DEFAULT_DEVICE_ID || protocolStore.containsSession(SignalProtocolAddress(serviceId.libSignalServiceId, it)) } - .sorted() - .toList() + return devices.sorted() } /** diff --git a/lib/network/src/test/java/org/signal/network/service/MessageServiceTest.kt b/lib/network/src/test/java/org/signal/network/service/MessageServiceTest.kt index ed980cee89..68c53c0a15 100644 --- a/lib/network/src/test/java/org/signal/network/service/MessageServiceTest.kt +++ b/lib/network/src/test/java/org/signal/network/service/MessageServiceTest.kt @@ -228,6 +228,23 @@ class MessageServiceTest { verify(exactly = 0) { cipher.encrypt(SignalProtocolAddress(recipientAci.libSignalServiceId, 3), any(), any()) } } + @Test + fun `local send with no other devices sends to own device`() = runTest { + val service = newService() + val localProtocolAddress = SignalProtocolAddress(localAci.libSignalServiceId, SignalServiceAddress.DEFAULT_DEVICE_ID) + every { protocolStore.getSubDeviceSessions(localAci.toString()) } returns emptyList() + every { cipher.encrypt(any(), any(), any()) } returns OutgoingPushMessage(1, 1, 100, "AAAA") + + coEvery { messageApi.sendSealedSenderMessage(any(), any(), any(), any(), any(), any()) } returns + RequestResult.Success(Unit) + + val result = service.sendMessage(localAci, envelopeContent, timestamp, sealedSenderAccess = null, story = true, isOnline = false) + + val success = (result as Either.Right).value + assertThat(success.devices).isEqualTo(listOf(SignalServiceAddress.DEFAULT_DEVICE_ID)) + verify { cipher.encrypt(localProtocolAddress, null, envelopeContent) } + } + @Test fun `MismatchedDeviceException archives extras and fetches missing prekeys`() = runTest { val service = newService()