From 76e92f29b92eca0042fe5a329bcbf52f6f8fd999 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 27 Oct 2025 09:52:17 -0400 Subject: [PATCH] Fix call requests to a PNI. --- .../securesms/jobs/ProfileKeySendJob.java | 13 ++++++ .../messages/CallMessageProcessor.kt | 21 ++++++++- .../securesms/messages/MessageDecryptor.kt | 2 +- .../BeginCallActionProcessorDelegate.java | 40 ++++++++++++++++- .../service/webrtc/GroupActionProcessor.java | 10 ++--- .../service/webrtc/WebRtcActionProcessor.java | 43 +++++++++++++++---- .../securesms/service/webrtc/WebRtcData.java | 21 ++++++--- 7 files changed, 129 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java index 70eb94ff6f..2b40cb4859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -56,6 +56,19 @@ public class ProfileKeySendJob extends BaseJob { ); } + /** + * Suitable for a 1:1 conversation or a GV1 group only. + * + * @param queueLimits True if you only want one of these to be run per person after decryptions + * are drained, otherwise false. + * + * @return The job that is created, or null if the threadId provided was invalid. + */ + @WorkerThread + public static @Nullable ProfileKeySendJob create(@NonNull Recipient recipient, boolean queueLimits) { + return create(SignalDatabase.threads().getOrCreateThreadIdFor(recipient), queueLimits); + } + /** * Suitable for a 1:1 conversation or a GV1 group only. * diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/CallMessageProcessor.kt index 32cc1cb212..a0e4cfef12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/CallMessageProcessor.kt @@ -1,12 +1,15 @@ package org.thoughtcrime.securesms.messages +import org.signal.core.util.orNull import org.signal.ringrtc.CallId import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.ProfileKeySendJob import org.thoughtcrime.securesms.messages.MessageContentProcessor.Companion.log import org.thoughtcrime.securesms.messages.MessageContentProcessor.Companion.warn import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.ringrtc.RemotePeer import org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata @@ -36,6 +39,21 @@ object CallMessageProcessor { ) { val callMessage = content.callMessage!! + if (metadata.destinationServiceId is ServiceId.PNI) { + if (RecipientUtil.isCallRequestAccepted(senderRecipient) && callMessage.offer != null) { + log(envelope.timestamp!!, "Received call offer message at our PNI from trusted sender, responding with profile and pni signature") + RecipientUtil.shareProfileIfFirstSecureMessage(senderRecipient) + ProfileKeySendJob.create(senderRecipient, false)?.let { AppDependencies.jobManager.add(it) } + } + + if (callMessage.offer != null) { + log(envelope.timestamp!!, "Call message at our PNI is an offer, continuing.") + } else { + log(envelope.timestamp!!, "Call message at our PNI is not an offer, ignoring.") + return + } + } + when { callMessage.offer != null -> handleCallOfferMessage(envelope, metadata, callMessage.offer!!, senderRecipient.id, serverDeliveredTimestamp) callMessage.answer != null -> handleCallAnswerMessage(envelope, metadata, callMessage.answer!!, senderRecipient.id) @@ -57,13 +75,14 @@ object CallMessageProcessor { } val remotePeer = RemotePeer(senderRecipientId, CallId(offerId)) - val remoteIdentityKey = AppDependencies.protocolStore.aci().identities().getIdentityRecord(senderRecipientId).map { (_, identityKey): IdentityRecord -> identityKey.serialize() }.get() + val remoteIdentityKey = AppDependencies.protocolStore.get(metadata.destinationServiceId).identities().getIdentityRecord(senderRecipientId).map { (_, identityKey): IdentityRecord -> identityKey.serialize() }.orNull() AppDependencies.signalCallManager .receivedOffer( CallMetadata(remotePeer, metadata.sourceDeviceId), OfferMetadata(offer.opaque?.toByteArray(), OfferMessage.Type.fromProto(offer.type!!)), ReceivedOfferMetadata( + metadata.destinationServiceId, remoteIdentityKey, envelope.serverTimestamp!!, serverDeliveredTimestamp diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt index 2525e38fe9..f3e1481ee1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -146,7 +146,7 @@ object MessageDecryptor { } val bufferedStore = bufferedProtocolStore.get(destination) - val localAddress = SignalServiceAddress(selfAci, SignalStore.account.e164) + val localAddress = SignalServiceAddress(destination, SignalStore.account.e164) val cipher = SignalServiceCipher(localAddress, SignalStore.account.deviceId, bufferedStore, ReentrantSessionLock.INSTANCE, SealedSenderAccessUtil.getCertificateValidator()) return try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java index 5bf1b23d00..c1e75730c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java @@ -4,15 +4,22 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; +import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallManager; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; +import org.thoughtcrime.securesms.database.CallTable; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; +import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; + +import java.nio.ByteBuffer; import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING; @@ -31,6 +38,38 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) { + if (!remotePeer.getRecipient().getHasAci()) { + Log.w(tag, "1:1 outgoing recipient is PNI only, send pseudo-call offer and terminate call"); + + remotePeer.setCallId(new CallId(ByteBuffer.wrap(Util.getSecretBytes(8)).getLong())); + + currentState = currentState.builder() + .actionProcessor(new IdleActionProcessor(webRtcInteractor)) + .changeCallInfoState() + .callRecipient(remotePeer.getRecipient()) + .callState(WebRtcViewModel.State.CALL_NEEDS_PERMISSION) + .putParticipant(remotePeer.getRecipient(), CallParticipant.EMPTY) + .build(); + + boolean isVideoOffer = OfferMessage.Type.VIDEO_CALL == offerType; + + SignalDatabase.calls().insertOneToOneCall(remotePeer.getCallId().longValue(), + System.currentTimeMillis(), + remotePeer.getId(), + isVideoOffer ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL, + CallTable.Direction.OUTGOING, + CallTable.Event.ONGOING); + + webRtcInteractor.insertMissedCall(remotePeer, System.currentTimeMillis(), isVideoOffer, CallTable.Event.NOT_ACCEPTED); + webRtcInteractor.postStateUpdate(currentState); + webRtcInteractor.sendCallMessage(remotePeer, SignalServiceCallMessage.forOffer(new OfferMessage(remotePeer.getCallId().longValue(), + offerType, + new byte[0]), + null)); + + return terminate(currentState, remotePeer); + } + remotePeer.setCallStartTimestamp(System.currentTimeMillis()); currentState = currentState.builder() @@ -60,7 +99,6 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { .build(); CallManager.CallMediaType callMediaType = WebRtcUtil.getCallMediaTypeFromOfferType(offerType); - try { webRtcInteractor.getCallManager().call(remotePeer, callMediaType, SignalStore.account().getDeviceId()); } catch (CallException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java index 7bdf5103c3..e1f78c0c15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -49,12 +49,12 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { this.actionProcessorFactory = actionProcessorFactory; } - protected @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState, - @NonNull WebRtcData.CallMetadata callMetadata, - @NonNull WebRtcData.OfferMetadata offerMetadata, - @NonNull WebRtcData.ReceivedOfferMetadata receivedOfferMetadata) + protected @NonNull WebRtcServiceState handleValidatedReceivedOffer(@NonNull WebRtcServiceState currentState, + @NonNull WebRtcData.CallMetadata callMetadata, + @NonNull WebRtcData.OfferMetadata offerMetadata, + @NonNull WebRtcData.ReceivedOfferMetadata receivedOfferMetadata) { - Log.i(tag, "handleReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice())); + Log.i(tag, "handleValidatedReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice())); Log.i(tag, "In a group call, send busy back to 1:1 call offer."); currentState.getActionProcessor().handleSendBusy(currentState, callMetadata, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 621f0d63c5..e849eec729 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -55,6 +55,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import java.util.Collection; @@ -167,16 +168,20 @@ public abstract class WebRtcActionProcessor { //region Incoming call - protected @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState, - @NonNull CallMetadata callMetadata, - @NonNull OfferMetadata offerMetadata, - @NonNull ReceivedOfferMetadata receivedOfferMetadata) + protected final @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState, + @NonNull CallMetadata callMetadata, + @NonNull OfferMetadata offerMetadata, + @NonNull ReceivedOfferMetadata receivedOfferMetadata) { Log.i(tag, "handleReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()) + " offer_type:" + offerMetadata.getOfferType()); - if (TelephonyUtil.isAnyPstnLineBusy(context)) { - Log.i(tag, "PSTN line is busy."); - currentState = currentState.getActionProcessor().handleSendBusy(currentState, callMetadata, true); + if (receivedOfferMetadata.getDestinationServiceId() instanceof ServiceId.PNI) { + if (RecipientUtil.isCallRequestAccepted(callMetadata.getRemotePeer().getRecipient())) { + Log.i(tag, "Caller is trusted but called our PNI, insert missed call and send hangup as we can't proceed."); + currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NORMAL), true); + } else { + Log.i(tag, "Caller is untrusted but called our PNI, insert missed call and do not send hangup."); + } webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), receivedOfferMetadata.getServerReceivedTimestamp(), offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL); return currentState; } @@ -188,6 +193,13 @@ public abstract class WebRtcActionProcessor { return currentState; } + if (TelephonyUtil.isAnyPstnLineBusy(context)) { + Log.i(tag, "PSTN line is busy."); + currentState = currentState.getActionProcessor().handleSendBusy(currentState, callMetadata, true); + webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), receivedOfferMetadata.getServerReceivedTimestamp(), offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL); + return currentState; + } + if (offerMetadata.getOpaque() == null) { Log.w(tag, "Opaque data is required."); currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NORMAL), true); @@ -195,6 +207,13 @@ public abstract class WebRtcActionProcessor { return currentState; } + if (receivedOfferMetadata.getRemoteIdentityKey() == null) { + Log.w(tag, "Unable to locate remote identity key for caller, bailing"); + currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NORMAL), true); + webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), receivedOfferMetadata.getServerReceivedTimestamp(), offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL); + return currentState; + } + NotificationProfile activeProfile = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles().getProfiles()); if (activeProfile != null && !(activeProfile.isRecipientAllowed(callMetadata.getRemotePeer().getId()) || activeProfile.getAllowAllCalls())) { Log.w(tag, "Caller is excluded by notification profile."); @@ -202,7 +221,15 @@ public abstract class WebRtcActionProcessor { return currentState; } - Log.i(tag, "add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode()); + return handleValidatedReceivedOffer(currentState, callMetadata, offerMetadata, receivedOfferMetadata); + } + + protected @NonNull WebRtcServiceState handleValidatedReceivedOffer(@NonNull WebRtcServiceState currentState, + @NonNull CallMetadata callMetadata, + @NonNull OfferMetadata offerMetadata, + @NonNull ReceivedOfferMetadata receivedOfferMetadata) + { + Log.i(tag, "handleValidatedReceivedOffer(): add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode()); callMetadata.getRemotePeer().setCallStartTimestamp(receivedOfferMetadata.getServerReceivedTimestamp()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java index e111cc3ace..f215b6ac74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java @@ -8,6 +8,7 @@ import org.signal.ringrtc.CallManager; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; +import org.whispersystems.signalservice.api.push.ServiceId; import java.util.UUID; @@ -66,17 +67,27 @@ public class WebRtcData { * Additional metadata for a received call. */ public static class ReceivedOfferMetadata { - private final @NonNull byte[] remoteIdentityKey; - private final long serverReceivedTimestamp; - private final long serverDeliveredTimestamp; + private final @NonNull ServiceId destinationServiceId; + private final @Nullable byte[] remoteIdentityKey; + private final long serverReceivedTimestamp; + private final long serverDeliveredTimestamp; - public ReceivedOfferMetadata(@NonNull byte[] remoteIdentityKey, long serverReceivedTimestamp, long serverDeliveredTimestamp) { + public ReceivedOfferMetadata(@NonNull ServiceId destinationServiceId, + @Nullable byte[] remoteIdentityKey, + long serverReceivedTimestamp, + long serverDeliveredTimestamp) + { + this.destinationServiceId = destinationServiceId; this.remoteIdentityKey = remoteIdentityKey; this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; } - @NonNull byte[] getRemoteIdentityKey() { + @NonNull ServiceId getDestinationServiceId() { + return destinationServiceId; + } + + @Nullable byte[] getRemoteIdentityKey() { return remoteIdentityKey; }