Fix call requests to a PNI.

This commit is contained in:
Cody Henthorne
2025-10-27 09:52:17 -04:00
committed by jeffrey-signal
parent 55617c18f0
commit 76e92f29b9
7 changed files with 129 additions and 21 deletions

View File

@@ -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.
*

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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());

View File

@@ -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;
}