kopia lustrzana https://github.com/ryukoposting/Signal-Android
Initial refactor of the message decryption flow.
rodzic
c1a94be9cd
commit
ec2565263e
|
@ -1,242 +0,0 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState;
|
||||
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil;
|
||||
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Decrypts an envelope. Enqueues a separate job, {@link PushProcessMessageJob}, to actually insert
|
||||
* the result into our database.
|
||||
*/
|
||||
public final class PushDecryptMessageJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "PushDecryptJob";
|
||||
public static final String QUEUE = "__PUSH_DECRYPT_JOB__";
|
||||
|
||||
public static final String TAG = Log.tag(PushDecryptMessageJob.class);
|
||||
|
||||
private static final String KEY_SMS_MESSAGE_ID = "sms_message_id";
|
||||
private static final String KEY_ENVELOPE = "envelope";
|
||||
|
||||
private final long smsMessageId;
|
||||
private final SignalServiceEnvelope envelope;
|
||||
|
||||
public PushDecryptMessageJob(Context context, @NonNull SignalServiceEnvelope envelope) {
|
||||
this(context, envelope, -1);
|
||||
}
|
||||
|
||||
public PushDecryptMessageJob(Context context, @NonNull SignalServiceEnvelope envelope, long smsMessageId) {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue(QUEUE)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
envelope,
|
||||
smsMessageId);
|
||||
setContext(context);
|
||||
}
|
||||
|
||||
private PushDecryptMessageJob(@NonNull Parameters parameters, @NonNull SignalServiceEnvelope envelope, long smsMessageId) {
|
||||
super(parameters);
|
||||
|
||||
this.envelope = envelope;
|
||||
this.smsMessageId = smsMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldTrace() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putBlobAsString(KEY_ENVELOPE, envelope.serialize())
|
||||
.putLong(KEY_SMS_MESSAGE_ID, smsMessageId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws RetryLaterException {
|
||||
if (needsMigration()) {
|
||||
Log.w(TAG, "Migration is still needed.");
|
||||
postMigrationNotification();
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
List<Job> jobs = new LinkedList<>();
|
||||
DecryptionResult result = MessageDecryptionUtil.decrypt(context, envelope);
|
||||
|
||||
if (result.getState() == MessageState.DECRYPTED_OK && envelope.isStory() && !isStoryMessage(result)) {
|
||||
Log.w(TAG, "Envelope was flagged as a story, but it did not have any story-related content! Dropping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getContent() != null) {
|
||||
if (result.getContent().getSenderKeyDistributionMessage().isPresent()) {
|
||||
handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get());
|
||||
}
|
||||
|
||||
if (FeatureFlags.phoneNumberPrivacy() && result.getContent().getPniSignatureMessage().isPresent()) {
|
||||
handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get());
|
||||
} else if (result.getContent().getPniSignatureMessage().isPresent()) {
|
||||
Log.w(TAG, "Ignoring PNI signature because the feature flag is disabled!");
|
||||
}
|
||||
|
||||
if (envelope.hasReportingToken() && envelope.getReportingToken() != null && envelope.getReportingToken().length > 0) {
|
||||
SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken());
|
||||
}
|
||||
|
||||
jobs.add(new PushProcessMessageJob(result.getContent(), smsMessageId, envelope.getTimestamp()));
|
||||
} else if (result.getException() != null && result.getState() != MessageState.NOOP) {
|
||||
jobs.add(new PushProcessMessageJob(result.getState(), result.getException(), smsMessageId, envelope.getTimestamp()));
|
||||
}
|
||||
|
||||
jobs.addAll(result.getJobs());
|
||||
|
||||
for (Job job: jobs) {
|
||||
ApplicationDependencies.getJobManager().add(job);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return exception instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private void handleSenderKeyDistributionMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SenderKeyDistributionMessage message) {
|
||||
Log.i(TAG, "Processing SenderKeyDistributionMessage from " + address.getServiceId() + "." + deviceId);
|
||||
SignalServiceMessageSender sender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
sender.processSenderKeyDistributionMessage(new SignalProtocolAddress(address.getIdentifier(), deviceId), message);
|
||||
}
|
||||
|
||||
private void handlePniSignatureMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SignalServicePniSignatureMessage pniSignatureMessage) {
|
||||
Log.i(TAG, "Processing PniSignatureMessage from " + address.getServiceId() + "." + deviceId);
|
||||
|
||||
PNI pni = pniSignatureMessage.getPni();
|
||||
|
||||
if (SignalDatabase.recipients().isAssociated(address.getServiceId(), pni)) {
|
||||
Log.i(TAG, "[handlePniSignatureMessage] ACI (" + address.getServiceId() + ") and PNI (" + pni + ") are already associated.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
|
||||
SignalProtocolAddress aciAddress = new SignalProtocolAddress(address.getIdentifier(), deviceId);
|
||||
SignalProtocolAddress pniAddress = new SignalProtocolAddress(pni.toString(), deviceId);
|
||||
IdentityKey aciIdentity = identityStore.getIdentity(aciAddress);
|
||||
IdentityKey pniIdentity = identityStore.getIdentity(pniAddress);
|
||||
|
||||
if (aciIdentity == null) {
|
||||
Log.w(TAG, "[validatePniSignature] No identity found for ACI address " + aciAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pniIdentity == null) {
|
||||
Log.w(TAG, "[validatePniSignature] No identity found for PNI address " + pniAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pniIdentity.verifyAlternateIdentity(aciIdentity, pniSignatureMessage.getSignature())) {
|
||||
Log.i(TAG, "[validatePniSignature] PNI signature is valid. Associating ACI (" + address.getServiceId() + ") with PNI (" + pni + ")");
|
||||
SignalDatabase.recipients().getAndPossiblyMergePnpVerified(address.getServiceId(), pni, address.getNumber().orElse(null));
|
||||
} else {
|
||||
Log.w(TAG, "[validatePniSignature] Invalid PNI signature! Cannot associate ACI (" + address.getServiceId() + ") with PNI (" + pni + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStoryMessage(@NonNull DecryptionResult result) {
|
||||
if (result.getContent() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.getContent().getSenderKeyDistributionMessage().isPresent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.getContent().getStoryMessage().isPresent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.getContent().getDataMessage().isPresent() &&
|
||||
result.getContent().getDataMessage().get().getStoryContext().isPresent() &&
|
||||
result.getContent().getDataMessage().get().getGroupContext().isPresent())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.getContent().getDataMessage().isPresent() &&
|
||||
result.getContent().getDataMessage().get().getRemoteDelete().isPresent())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean needsMigration() {
|
||||
return TextSecurePreferences.getNeedsSqlCipherMigration(context);
|
||||
}
|
||||
|
||||
private void postMigrationNotification() {
|
||||
NotificationManagerCompat.from(context).notify(NotificationIds.LEGACY_SQLCIPHER_MIGRATION,
|
||||
new NotificationCompat.Builder(context, NotificationChannels.getInstance().getMessagesChannel())
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
|
||||
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), PendingIntentFlags.mutable()))
|
||||
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<PushDecryptMessageJob> {
|
||||
@Override
|
||||
public @NonNull PushDecryptMessageJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new PushDecryptMessageJob(parameters,
|
||||
SignalServiceEnvelope.deserialize(data.getStringAsBlob(KEY_ENVELOPE)),
|
||||
data.getLong(KEY_SMS_MESSAGE_ID));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.app.PendingIntent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.signal.core.util.PendingIntentFlags.mutable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState
|
||||
import org.thoughtcrime.securesms.messages.MessageDecryptor
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Decrypts an envelope. Enqueues a separate job, [PushProcessMessageJob], to actually insert
|
||||
* the result into our database.
|
||||
*/
|
||||
class PushDecryptMessageJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val envelope: SignalServiceEnvelope,
|
||||
private val smsMessageId: Long
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(PushDecryptMessageJob::class.java)
|
||||
|
||||
const val KEY = "PushDecryptJob"
|
||||
const val QUEUE = "__PUSH_DECRYPT_JOB__"
|
||||
|
||||
private const val KEY_SMS_MESSAGE_ID = "sms_message_id"
|
||||
private const val KEY_ENVELOPE = "envelope"
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
constructor(envelope: SignalServiceEnvelope, smsMessageId: Long = -1) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(QUEUE)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
envelope,
|
||||
smsMessageId
|
||||
)
|
||||
|
||||
override fun shouldTrace() = true
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putBlobAsString(KEY_ENVELOPE, envelope.serialize())
|
||||
.putLong(KEY_SMS_MESSAGE_ID, smsMessageId)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey() = KEY
|
||||
|
||||
@Throws(RetryLaterException::class)
|
||||
public override fun onRun() {
|
||||
if (needsMigration()) {
|
||||
Log.w(TAG, "Migration is still needed.")
|
||||
postMigrationNotification()
|
||||
throw RetryLaterException()
|
||||
}
|
||||
|
||||
val result = MessageDecryptor.decrypt(context, envelope.proto, envelope.serverDeliveredTimestamp)
|
||||
|
||||
when (result) {
|
||||
is MessageDecryptor.Result.Success -> {
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
PushProcessMessageJob(
|
||||
result.toMessageState(),
|
||||
result.toSignalServiceContent(),
|
||||
null,
|
||||
smsMessageId,
|
||||
result.envelope.timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is MessageDecryptor.Result.Error -> {
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
PushProcessMessageJob(
|
||||
result.toMessageState(),
|
||||
null,
|
||||
result.errorMetadata.toExceptionMetadata(),
|
||||
smsMessageId,
|
||||
result.envelope.timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is MessageDecryptor.Result.Ignore -> {
|
||||
// No action needed
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw AssertionError("Unexpected result! ${result.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
result.followUpOperations.forEach { it.run() }
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(exception: Exception): Boolean {
|
||||
return exception is RetryLaterException
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
private fun needsMigration(): Boolean {
|
||||
return TextSecurePreferences.getNeedsSqlCipherMigration(context)
|
||||
}
|
||||
|
||||
private fun MessageDecryptor.Result.toMessageState(): MessageState {
|
||||
return when (this) {
|
||||
is MessageDecryptor.Result.DecryptionError -> MessageState.DECRYPTION_ERROR
|
||||
is MessageDecryptor.Result.Ignore -> MessageState.NOOP
|
||||
is MessageDecryptor.Result.InvalidVersion -> MessageState.INVALID_VERSION
|
||||
is MessageDecryptor.Result.LegacyMessage -> MessageState.LEGACY_MESSAGE
|
||||
is MessageDecryptor.Result.Success -> MessageState.DECRYPTED_OK
|
||||
is MessageDecryptor.Result.UnsupportedDataMessage -> MessageState.UNSUPPORTED_DATA_MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageDecryptor.Result.Success.toSignalServiceContent(): SignalServiceContent {
|
||||
val localAddress = SignalServiceAddress(this.metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
|
||||
val metadata = SignalServiceMetadata(
|
||||
SignalServiceAddress(this.metadata.sourceServiceId, Optional.ofNullable(this.metadata.sourceE164)),
|
||||
this.metadata.sourceDeviceId,
|
||||
this.envelope.timestamp,
|
||||
this.envelope.serverTimestamp,
|
||||
this.serverDeliveredTimestamp,
|
||||
this.metadata.sealedSender,
|
||||
this.envelope.serverGuid,
|
||||
Optional.ofNullable(this.metadata.groupId),
|
||||
this.metadata.destinationServiceId.toString()
|
||||
)
|
||||
|
||||
val contentProto = SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
|
||||
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(metadata))
|
||||
.setContent(content)
|
||||
.build()
|
||||
|
||||
return SignalServiceContent.createFromProto(contentProto)!!
|
||||
}
|
||||
|
||||
private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): ExceptionMetadata {
|
||||
return ExceptionMetadata(
|
||||
this.sender,
|
||||
this.senderDevice,
|
||||
this.groupId
|
||||
)
|
||||
}
|
||||
|
||||
private fun postMigrationNotification() {
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().messagesChannel)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
|
||||
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), mutable()))
|
||||
.setDefaults(NotificationCompat.DEFAULT_SOUND or NotificationCompat.DEFAULT_VIBRATE)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NotificationIds.LEGACY_SQLCIPHER_MIGRATION, notification)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<PushDecryptMessageJob> {
|
||||
override fun create(parameters: Parameters, data: Data): PushDecryptMessageJob {
|
||||
return PushDecryptMessageJob(
|
||||
parameters,
|
||||
SignalServiceEnvelope.deserialize(data.getStringAsBlob(KEY_ENVELOPE)),
|
||||
data.getLong(KEY_SMS_MESSAGE_ID)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,31 +51,6 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
private final long smsMessageId;
|
||||
private final long timestamp;
|
||||
|
||||
@WorkerThread
|
||||
PushProcessMessageJob(@NonNull SignalServiceContent content,
|
||||
long smsMessageId,
|
||||
long timestamp)
|
||||
{
|
||||
this(MessageState.DECRYPTED_OK,
|
||||
content,
|
||||
null,
|
||||
smsMessageId,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
PushProcessMessageJob(@NonNull MessageState messageState,
|
||||
@NonNull ExceptionMetadata exceptionMetadata,
|
||||
long smsMessageId,
|
||||
long timestamp)
|
||||
{
|
||||
this(messageState,
|
||||
null,
|
||||
exceptionMetadata,
|
||||
smsMessageId,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public PushProcessMessageJob(@NonNull MessageState messageState,
|
||||
@Nullable SignalServiceContent content,
|
||||
|
|
|
@ -7,30 +7,24 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
|
||||
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
|
@ -97,62 +91,11 @@ public class IncomingMessageProcessor {
|
|||
}
|
||||
|
||||
private @Nullable String processMessageDeferred(@NonNull SignalServiceEnvelope envelope) {
|
||||
Job job = new PushDecryptMessageJob(context, envelope);
|
||||
Job job = new PushDecryptMessageJob(envelope);
|
||||
jobManager.add(job);
|
||||
return job.getId();
|
||||
}
|
||||
|
||||
private @Nullable String processMessageInline(@NonNull SignalServiceEnvelope envelope) {
|
||||
Log.i(TAG, "Received message " + envelope.getTimestamp() + ".");
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("message");
|
||||
|
||||
if (needsToEnqueueDecryption()) {
|
||||
Log.d(TAG, "Need to enqueue decryption.");
|
||||
PushDecryptMessageJob job = new PushDecryptMessageJob(context, envelope);
|
||||
jobManager.add(job);
|
||||
return job.getId();
|
||||
}
|
||||
|
||||
stopwatch.split("queue-check");
|
||||
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
Log.i(TAG, "Acquired lock while processing message " + envelope.getTimestamp() + ".");
|
||||
|
||||
DecryptionResult result = MessageDecryptionUtil.decrypt(context, envelope);
|
||||
Log.d(TAG, "Decryption finished for " + envelope.getTimestamp());
|
||||
stopwatch.split("decrypt");
|
||||
|
||||
for (Job job : result.getJobs()) {
|
||||
jobManager.add(job);
|
||||
}
|
||||
|
||||
stopwatch.split("jobs");
|
||||
|
||||
if (needsToEnqueueProcessing(result)) {
|
||||
Log.d(TAG, "Need to enqueue processing.");
|
||||
jobManager.add(new PushProcessMessageJob(result.getState(), result.getContent(), result.getException(), -1, envelope.getTimestamp()));
|
||||
return null;
|
||||
}
|
||||
|
||||
stopwatch.split("group-check");
|
||||
|
||||
try {
|
||||
MessageContentProcessor processor = MessageContentProcessor.create(context);
|
||||
processor.process(result.getState(), result.getContent(), result.getException(), envelope.getTimestamp(), -1);
|
||||
return null;
|
||||
} catch (IOException | GroupChangeBusyException e) {
|
||||
Log.w(TAG, "Exception during message processing.", e);
|
||||
jobManager.add(new PushProcessMessageJob(result.getState(), result.getContent(), result.getException(), -1, envelope.getTimestamp()));
|
||||
}
|
||||
} finally {
|
||||
stopwatch.split("process");
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
|
||||
Recipient sender = Recipient.externalPush(envelope.getSourceAddress());
|
||||
Log.i(TAG, "Received server receipt. Sender: " + sender.getId() + ", Device: " + envelope.getSourceDevice() + ", Timestamp: " + envelope.getTimestamp());
|
||||
|
@ -161,42 +104,6 @@ public class IncomingMessageProcessor {
|
|||
SignalDatabase.messageLog().deleteEntryForRecipient(envelope.getTimestamp(), sender.getId(), envelope.getSourceDevice());
|
||||
}
|
||||
|
||||
private boolean needsToEnqueueDecryption() {
|
||||
return !jobManager.areQueuesEmpty(SetUtil.newHashSet(Job.Parameters.MIGRATION_QUEUE_KEY, PushDecryptMessageJob.QUEUE)) ||
|
||||
TextSecurePreferences.getNeedsSqlCipherMigration(context);
|
||||
}
|
||||
|
||||
private boolean needsToEnqueueProcessing(@NonNull DecryptionResult result) {
|
||||
SignalServiceGroupV2 groupContext = GroupUtil.getGroupContextIfPresent(result.getContent());
|
||||
|
||||
if (groupContext != null) {
|
||||
GroupId groupId = GroupId.v2(groupContext.getMasterKey());
|
||||
|
||||
if (groupId.isV2()) {
|
||||
String queueName = PushProcessMessageJob.getQueueName(Recipient.externalPossiblyMigratedGroup(groupId).getId());
|
||||
GroupTable groupDatabase = SignalDatabase.groups();
|
||||
|
||||
return !jobManager.isQueueEmpty(queueName) ||
|
||||
groupContext.getRevision() > groupDatabase.getGroupV2Revision(groupId.requireV2()) ||
|
||||
groupDatabase.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (result.getContent() != null) {
|
||||
RecipientId recipientId = RecipientId.from(result.getContent().getSender());
|
||||
String queueKey = PushProcessMessageJob.getQueueName(recipientId);
|
||||
|
||||
return !jobManager.isQueueEmpty(queueKey);
|
||||
} else if (result.getException() != null) {
|
||||
RecipientId recipientId = Recipient.external(context, result.getException().getSender()).getId();
|
||||
String queueKey = PushProcessMessageJob.getQueueName(recipientId);
|
||||
|
||||
return !jobManager.isQueueEmpty(queueKey);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
release();
|
||||
|
|
|
@ -586,6 +586,11 @@ public final class MessageContentProcessor {
|
|||
}
|
||||
|
||||
switch (messageState) {
|
||||
case DECRYPTION_ERROR:
|
||||
warn(String.valueOf(timestamp), "Handling encryption error.");
|
||||
SignalDatabase.messages().insertBadDecryptMessage(sender.getId(), e.senderDevice, timestamp, System.currentTimeMillis(), getThreadIdForException(e));
|
||||
break;
|
||||
|
||||
case INVALID_VERSION:
|
||||
warn(String.valueOf(timestamp), "Handling invalid version.");
|
||||
handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
||||
|
@ -616,6 +621,15 @@ public final class MessageContentProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private long getThreadIdForException(ExceptionMetadata metadata) {
|
||||
if (metadata.groupId != null) {
|
||||
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(metadata.groupId);
|
||||
return SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
|
||||
} else {
|
||||
return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.external(context, metadata.sender));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallOfferMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull OfferMessage message,
|
||||
@NonNull Optional<Long> smsMessageId,
|
||||
|
@ -3406,7 +3420,8 @@ public final class MessageContentProcessor {
|
|||
LEGACY_MESSAGE,
|
||||
DUPLICATE_MESSAGE,
|
||||
UNSUPPORTED_DATA_MESSAGE,
|
||||
NOOP
|
||||
NOOP,
|
||||
DECRYPTION_ERROR
|
||||
}
|
||||
|
||||
public static final class ExceptionMetadata {
|
||||
|
|
|
@ -1,308 +0,0 @@
|
|||
package org.thoughtcrime.securesms.messages;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
|
||||
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
|
||||
import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendRetryReceiptJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handles taking an encrypted {@link SignalServiceEnvelope} and turning it into a plaintext model.
|
||||
*/
|
||||
public final class MessageDecryptionUtil {
|
||||
|
||||
private static final String TAG = Log.tag(MessageDecryptionUtil.class);
|
||||
|
||||
private MessageDecryptionUtil() {}
|
||||
|
||||
/**
|
||||
* Takes a {@link SignalServiceEnvelope} and returns a {@link DecryptionResult}, which has either
|
||||
* a plaintext {@link SignalServiceContent} or information about an error that happened.
|
||||
*
|
||||
* Excluding the data updated in our protocol stores that results from decrypting a message, this
|
||||
* method is side-effect free, preferring to return the decryption results to be handled by the
|
||||
* caller.
|
||||
*/
|
||||
public static @NonNull DecryptionResult decrypt(@NonNull Context context, @NonNull SignalServiceEnvelope envelope) {
|
||||
ServiceId aci = SignalStore.account().requireAci();
|
||||
ServiceId pni = SignalStore.account().requirePni();
|
||||
|
||||
ServiceId destination;
|
||||
if (!FeatureFlags.phoneNumberPrivacy()) {
|
||||
destination = aci;
|
||||
} else if (envelope.hasDestinationUuid()) {
|
||||
destination = ServiceId.parseOrThrow(envelope.getDestinationUuid());
|
||||
} else {
|
||||
Log.w(TAG, "No destinationUuid set! Defaulting to ACI.");
|
||||
destination = aci;
|
||||
}
|
||||
|
||||
if (destination.equals(pni)) {
|
||||
if (envelope.hasSourceUuid()) {
|
||||
RecipientId sender = RecipientId.from(envelope.getSourceAddress());
|
||||
SignalDatabase.recipients().markNeedsPniSignature(sender);
|
||||
} else {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Got a sealed sender message to our PNI? Invalid message, ignoring.");
|
||||
return DecryptionResult.forNoop(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
if (!destination.equals(aci) && !destination.equals(pni)) {
|
||||
Log.w(TAG, "Destination of " + destination + " does not match our ACI (" + aci + ") or PNI (" + pni + ")! Defaulting to ACI.");
|
||||
destination = aci;
|
||||
}
|
||||
|
||||
SignalServiceAccountDataStore protocolStore = ApplicationDependencies.getProtocolStore().get(destination);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(SignalStore.account().requireAci(), SignalStore.account().getE164());
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, SignalStore.account().getDeviceId(), protocolStore, ReentrantSessionLock.INSTANCE, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
List<Job> jobs = new LinkedList<>();
|
||||
|
||||
if (envelope.isPreKeySignalMessage()) {
|
||||
PreKeysSyncJob.enqueue();
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
return DecryptionResult.forSuccess(cipher.decrypt(envelope), jobs);
|
||||
} catch (ProtocolInvalidVersionException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
return DecryptionResult.forError(MessageState.INVALID_VERSION, toExceptionMetadata(e), jobs);
|
||||
|
||||
} catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException | ProtocolInvalidMessageException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e, true);
|
||||
Recipient sender = Recipient.external(context, e.getSender());
|
||||
|
||||
if (FeatureFlags.retryReceipts()) {
|
||||
jobs.add(handleRetry(context, sender, envelope, e));
|
||||
postInternalErrorNotification(context);
|
||||
} else {
|
||||
jobs.add(new AutomaticSessionResetJob(sender.getId(), e.getSenderDevice(), envelope.getTimestamp()));
|
||||
}
|
||||
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
} catch (ProtocolLegacyMessageException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] " + envelope.getSourceIdentifier() + ":" + envelope.getSourceDevice(), e);
|
||||
return DecryptionResult.forError(MessageState.LEGACY_MESSAGE, toExceptionMetadata(e), jobs);
|
||||
} catch (ProtocolDuplicateMessageException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] " + envelope.getSourceIdentifier() + ":" + envelope.getSourceDevice(), e);
|
||||
return DecryptionResult.forError(MessageState.DUPLICATE_MESSAGE, toExceptionMetadata(e), jobs);
|
||||
} catch (InvalidMetadataVersionException | InvalidMetadataMessageException | InvalidMessageStructureException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] " + envelope.getSourceIdentifier() + ":" + envelope.getSourceDevice(), e);
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
} catch (SelfSendException e) {
|
||||
Log.i(TAG, "Dropping UD message from self.");
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
} catch (UnsupportedDataMessageException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] " + envelope.getSourceIdentifier() + ":" + envelope.getSourceDevice(), e);
|
||||
return DecryptionResult.forError(MessageState.UNSUPPORTED_DATA_MESSAGE, toExceptionMetadata(e), jobs);
|
||||
}
|
||||
} catch (NoSenderException e) {
|
||||
Log.w(TAG, "Invalid message, but no sender info!");
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Job handleRetry(@NonNull Context context, @NonNull Recipient sender, @NonNull SignalServiceEnvelope envelope, @NonNull ProtocolException protocolException) {
|
||||
ContentHint contentHint = ContentHint.fromType(protocolException.getContentHint());
|
||||
int senderDevice = protocolException.getSenderDevice();
|
||||
long receivedTimestamp = System.currentTimeMillis();
|
||||
Optional<GroupId> groupId = Optional.empty();
|
||||
|
||||
if (protocolException.getGroupId().isPresent()) {
|
||||
try {
|
||||
groupId = Optional.of(GroupId.push(protocolException.getGroupId().get()));
|
||||
} catch (BadGroupIdException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Bad groupId!", true);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Could not decrypt a message with a type of " + contentHint, true);
|
||||
|
||||
long threadId;
|
||||
|
||||
if (groupId.isPresent()) {
|
||||
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId.get());
|
||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
|
||||
} else {
|
||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(sender);
|
||||
}
|
||||
|
||||
switch (contentHint) {
|
||||
case DEFAULT:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting an error right away because it's " + contentHint, true);
|
||||
SignalDatabase.messages().insertBadDecryptMessage(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId);
|
||||
break;
|
||||
case RESENDABLE:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting into pending retries store because it's " + contentHint, true);
|
||||
ApplicationDependencies.getPendingRetryReceiptCache().insert(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId);
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
break;
|
||||
case IMPLICIT:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Not inserting any error because it's " + contentHint, true);
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] originalContent;
|
||||
int envelopeType;
|
||||
if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) {
|
||||
originalContent = protocolException.getUnidentifiedSenderMessageContent().get().getContent();
|
||||
envelopeType = protocolException.getUnidentifiedSenderMessageContent().get().getType();
|
||||
} else {
|
||||
originalContent = envelope.getContent();
|
||||
envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType());
|
||||
}
|
||||
|
||||
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, envelopeType, envelope.getTimestamp(), senderDevice);
|
||||
|
||||
return new SendRetryReceiptJob(sender.getId(), groupId, decryptionErrorMessage);
|
||||
}
|
||||
|
||||
private static ExceptionMetadata toExceptionMetadata(@NonNull UnsupportedDataMessageException e)
|
||||
throws NoSenderException
|
||||
{
|
||||
String sender = e.getSender();
|
||||
|
||||
if (sender == null) throw new NoSenderException();
|
||||
|
||||
GroupId groupId = e.getGroup().isPresent() ? GroupId.v2(e.getGroup().get().getMasterKey()) : null;
|
||||
|
||||
return new ExceptionMetadata(sender, e.getSenderDevice(), groupId);
|
||||
}
|
||||
|
||||
private static ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException {
|
||||
String sender = e.getSender();
|
||||
|
||||
if (sender == null) throw new NoSenderException();
|
||||
|
||||
return new ExceptionMetadata(sender, e.getSenderDevice());
|
||||
}
|
||||
|
||||
private static void postInternalErrorNotification(@NonNull Context context) {
|
||||
if (!FeatureFlags.internalUser()) return;
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR,
|
||||
new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.getString(R.string.MessageDecryptionUtil_failed_to_decrypt_message))
|
||||
.setContentText(context.getString(R.string.MessageDecryptionUtil_tap_to_send_a_debug_log))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, SubmitDebugLogActivity.class), PendingIntentFlags.mutable()))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static int envelopeTypeToCiphertextMessageType(int envelopeType) {
|
||||
switch (envelopeType) {
|
||||
case SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE: return CiphertextMessage.WHISPER_TYPE;
|
||||
case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE: return CiphertextMessage.PREKEY_TYPE;
|
||||
case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE: return CiphertextMessage.SENDERKEY_TYPE;
|
||||
case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE: return CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
||||
default: return CiphertextMessage.WHISPER_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class NoSenderException extends Exception {}
|
||||
|
||||
public static class DecryptionResult {
|
||||
private final @NonNull MessageState state;
|
||||
private final @Nullable SignalServiceContent content;
|
||||
private final @Nullable ExceptionMetadata exception;
|
||||
private final @NonNull List<Job> jobs;
|
||||
|
||||
static @NonNull DecryptionResult forSuccess(@NonNull SignalServiceContent content, @NonNull List<Job> jobs) {
|
||||
return new DecryptionResult(MessageState.DECRYPTED_OK, content, null, jobs);
|
||||
}
|
||||
|
||||
static @NonNull DecryptionResult forError(@NonNull MessageState messageState,
|
||||
@NonNull ExceptionMetadata exception,
|
||||
@NonNull List<Job> jobs)
|
||||
{
|
||||
return new DecryptionResult(messageState, null, exception, jobs);
|
||||
}
|
||||
|
||||
static @NonNull DecryptionResult forNoop(@NonNull List<Job> jobs) {
|
||||
return new DecryptionResult(MessageState.NOOP, null, null, jobs);
|
||||
}
|
||||
|
||||
private DecryptionResult(@NonNull MessageState state,
|
||||
@Nullable SignalServiceContent content,
|
||||
@Nullable ExceptionMetadata exception,
|
||||
@NonNull List<Job> jobs)
|
||||
{
|
||||
this.state = state;
|
||||
this.content = content;
|
||||
this.exception = exception;
|
||||
this.jobs = jobs;
|
||||
}
|
||||
|
||||
public @NonNull MessageState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public @Nullable SignalServiceContent getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public @Nullable ExceptionMetadata getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
public @NonNull List<Job> getJobs() {
|
||||
return jobs;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,498 @@
|
|||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.wire.internal.toUnmodifiableList
|
||||
import org.signal.core.util.PendingIntentFlags
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.metadata.InvalidMetadataMessageException
|
||||
import org.signal.libsignal.metadata.InvalidMetadataVersionException
|
||||
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException
|
||||
import org.signal.libsignal.metadata.ProtocolException
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidVersionException
|
||||
import org.signal.libsignal.metadata.ProtocolLegacyMessageException
|
||||
import org.signal.libsignal.metadata.ProtocolNoSessionException
|
||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException
|
||||
import org.signal.libsignal.metadata.SelfSendException
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.message.CiphertextMessage
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage
|
||||
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.SendRetryReceiptJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.PniSignatureMessage
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* This class is designed to handle everything around the process of taking an [Envelope] and decrypting it into something
|
||||
* that you can use (or provide an appropriate error if something goes wrong). We'll also use this space to go over some
|
||||
* high-level concepts in message decryption.
|
||||
*/
|
||||
object MessageDecryptor {
|
||||
|
||||
private val TAG = Log.tag(MessageDecryptor::class.java)
|
||||
|
||||
/**
|
||||
* Decrypts an envelope and provides a [Result]. This method has side effects, but all of them are limited to [SignalDatabase].
|
||||
* That means that this operation should be atomic when performed within a transaction.
|
||||
* To keep that property, there may be [Result.followUpOperations] you have to perform after your transaction is committed.
|
||||
* These can vary from enqueueing jobs to inserting items into the [org.thoughtcrime.securesms.database.PendingRetryReceiptCache].
|
||||
*/
|
||||
fun decrypt(context: Context, envelope: Envelope, serverDeliveredTimestamp: Long): Result {
|
||||
val selfAci: ServiceId = SignalStore.account().requireAci()
|
||||
val selfPni: ServiceId = SignalStore.account().requirePni()
|
||||
|
||||
val destination: ServiceId = envelope.getDestination(selfAci, selfPni)
|
||||
|
||||
if (destination == selfPni && envelope.hasSourceUuid()) {
|
||||
Log.i(TAG, "${logPrefix(envelope)} Received a message at our PNI. Marking as needing a PNI signature.")
|
||||
|
||||
val sourceServiceId = ServiceId.parseOrNull(envelope.sourceUuid)
|
||||
|
||||
if (sourceServiceId != null) {
|
||||
val sender = RecipientId.from(sourceServiceId)
|
||||
SignalDatabase.recipients.markNeedsPniSignature(sender)
|
||||
} else {
|
||||
Log.w(TAG, "${logPrefix(envelope)} Could not mark sender as needing a PNI signature because the sender serviceId was invalid!")
|
||||
}
|
||||
}
|
||||
|
||||
if (destination == selfPni && !envelope.hasSourceUuid()) {
|
||||
Log.w(TAG, "${logPrefix(envelope)} Got a sealed sender message to our PNI? Invalid message, ignoring.")
|
||||
return Result.Ignore(envelope, serverDeliveredTimestamp, emptyList())
|
||||
}
|
||||
|
||||
val followUpOperations: MutableList<Runnable> = mutableListOf()
|
||||
|
||||
if (envelope.type == Envelope.Type.PREKEY_BUNDLE) {
|
||||
followUpOperations += Runnable {
|
||||
PreKeysSyncJob.enqueue()
|
||||
}
|
||||
}
|
||||
|
||||
val protocolStore: SignalServiceAccountDataStore = ApplicationDependencies.getProtocolStore().get(destination)
|
||||
val localAddress = SignalServiceAddress(selfAci, SignalStore.account().e164)
|
||||
val cipher = SignalServiceCipher(localAddress, SignalStore.account().deviceId, protocolStore, ReentrantSessionLock.INSTANCE, UnidentifiedAccessUtil.getCertificateValidator())
|
||||
|
||||
return try {
|
||||
val cipherResult: SignalServiceCipherResult? = cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||
|
||||
if (cipherResult == null) {
|
||||
Log.w(TAG, "${logPrefix(envelope)} Decryption resulted in a null result!", true)
|
||||
return Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations)
|
||||
}
|
||||
|
||||
Log.d(TAG, "${logPrefix(envelope, cipherResult)} Successfully decrypted the envelope.")
|
||||
|
||||
val validationResult: EnvelopeContentValidator.Result = EnvelopeContentValidator.validate(envelope, cipherResult.content)
|
||||
|
||||
if (validationResult is EnvelopeContentValidator.Result.Invalid) {
|
||||
Log.w(TAG, "${logPrefix(envelope, cipherResult)} Invalid content! ${validationResult.reason}", validationResult.throwable)
|
||||
return Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations)
|
||||
}
|
||||
|
||||
if (validationResult is EnvelopeContentValidator.Result.UnsupportedDataMessage) {
|
||||
Log.w(TAG, "${logPrefix(envelope, cipherResult)} Unsupported DataMessage! Our version: ${validationResult.ourVersion}, their version: ${validationResult.theirVersion}")
|
||||
return Result.UnsupportedDataMessage(envelope, serverDeliveredTimestamp, cipherResult.toErrorMetadata(), followUpOperations)
|
||||
}
|
||||
|
||||
// Must handle SKDM's immediately, because subsequent decryptions could rely on it
|
||||
if (cipherResult.content.hasSenderKeyDistributionMessage()) {
|
||||
handleSenderKeyDistributionMessage(
|
||||
envelope,
|
||||
cipherResult.metadata.sourceServiceId,
|
||||
cipherResult.metadata.sourceDeviceId,
|
||||
SenderKeyDistributionMessage(cipherResult.content.senderKeyDistributionMessage.toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
if (FeatureFlags.phoneNumberPrivacy() && cipherResult.content.hasPniSignatureMessage()) {
|
||||
handlePniSignatureMessage(
|
||||
envelope,
|
||||
cipherResult.metadata.sourceServiceId,
|
||||
cipherResult.metadata.sourceE164,
|
||||
cipherResult.metadata.sourceDeviceId,
|
||||
cipherResult.content.pniSignatureMessage
|
||||
)
|
||||
} else if (cipherResult.content.hasPniSignatureMessage()) {
|
||||
Log.w(TAG, "${logPrefix(envelope)} Ignoring PNI signature because the feature flag is disabled!")
|
||||
}
|
||||
|
||||
// TODO We can move this to the "message processing" stage once we give it access to the envelope. But for now it'll stay here.
|
||||
if (envelope.hasReportingToken() && envelope.reportingToken != null && envelope.reportingToken.size() > 0) {
|
||||
val sender = RecipientId.from(cipherResult.metadata.sourceServiceId)
|
||||
SignalDatabase.recipients.setReportingToken(sender, envelope.reportingToken.toByteArray())
|
||||
}
|
||||
|
||||
Result.Success(envelope, serverDeliveredTimestamp, cipherResult.content, cipherResult.metadata, followUpOperations.toUnmodifiableList())
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is ProtocolInvalidKeyIdException,
|
||||
is ProtocolInvalidKeyException,
|
||||
is ProtocolUntrustedIdentityException,
|
||||
is ProtocolNoSessionException,
|
||||
is ProtocolInvalidMessageException -> {
|
||||
check(e is ProtocolException)
|
||||
Log.w(TAG, "${logPrefix(envelope, e)} Decryption error!", e, true)
|
||||
|
||||
if (FeatureFlags.internalUser()) {
|
||||
postErrorNotification(context)
|
||||
}
|
||||
|
||||
if (FeatureFlags.retryReceipts()) {
|
||||
buildResultForDecryptionError(context, envelope, serverDeliveredTimestamp, followUpOperations, e)
|
||||
} else {
|
||||
Log.w(TAG, "${logPrefix(envelope, e)} Retry receipts disabled! Enqueuing a session reset job, which will also insert an error message.", e, true)
|
||||
|
||||
followUpOperations += Runnable {
|
||||
val sender: Recipient = Recipient.external(context, e.sender)
|
||||
ApplicationDependencies.getJobManager().add(AutomaticSessionResetJob(sender.id, e.senderDevice, envelope.timestamp))
|
||||
}
|
||||
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
}
|
||||
|
||||
is ProtocolDuplicateMessageException -> {
|
||||
Log.w(TAG, "${logPrefix(envelope, e)} Duplicate message!", e)
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
|
||||
is InvalidMetadataVersionException,
|
||||
is InvalidMetadataMessageException,
|
||||
is InvalidMessageStructureException -> {
|
||||
Log.w(TAG, "${logPrefix(envelope)} Invalid message structure!", e, true)
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
|
||||
is SelfSendException -> {
|
||||
Log.i(TAG, "[${envelope.timestamp}] Dropping sealed sender message from self!", e)
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
|
||||
is ProtocolInvalidVersionException -> {
|
||||
Log.w(TAG, "${logPrefix(envelope, e)} Invalid version!", e, true)
|
||||
Result.InvalidVersion(envelope, serverDeliveredTimestamp, e.toErrorMetadata(), followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
|
||||
is ProtocolLegacyMessageException -> {
|
||||
Log.w(TAG, "${logPrefix(envelope, e)} Legacy message!", e, true)
|
||||
Result.LegacyMessage(envelope, serverDeliveredTimestamp, e.toErrorMetadata(), followUpOperations)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Encountered an unexpected exception! Throwing!", e, true)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildResultForDecryptionError(
|
||||
context: Context,
|
||||
envelope: Envelope,
|
||||
serverDeliveredTimestamp: Long,
|
||||
followUpOperations: MutableList<Runnable>,
|
||||
protocolException: ProtocolException
|
||||
): Result {
|
||||
val contentHint: ContentHint = ContentHint.fromType(protocolException.contentHint)
|
||||
val senderDevice: Int = protocolException.senderDevice
|
||||
val receivedTimestamp: Long = System.currentTimeMillis()
|
||||
val sender: Recipient = Recipient.external(context, protocolException.sender)
|
||||
|
||||
followUpOperations += Runnable {
|
||||
ApplicationDependencies.getJobManager().add(buildSendRetryReceiptJob(envelope, protocolException, sender))
|
||||
}
|
||||
|
||||
return when (contentHint) {
|
||||
ContentHint.DEFAULT -> {
|
||||
Log.w(TAG, "${logPrefix(envelope)} The content hint is $contentHint, so we need to insert an error right away.", true)
|
||||
Result.DecryptionError(envelope, serverDeliveredTimestamp, protocolException.toErrorMetadata(), followUpOperations.toUnmodifiableList())
|
||||
}
|
||||
|
||||
ContentHint.RESENDABLE -> {
|
||||
Log.w(TAG, "${logPrefix(envelope)} The content hint is $contentHint, so we can try to resend the message.", true)
|
||||
|
||||
followUpOperations += Runnable {
|
||||
val groupId: GroupId? = protocolException.parseGroupId(envelope)
|
||||
val threadId: Long = if (groupId != null) {
|
||||
val groupRecipient: Recipient = Recipient.externalPossiblyMigratedGroup(groupId)
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
} else {
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(sender)
|
||||
}
|
||||
|
||||
ApplicationDependencies.getPendingRetryReceiptCache().insert(sender.id, senderDevice, envelope.timestamp, receivedTimestamp, threadId)
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary()
|
||||
}
|
||||
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations)
|
||||
}
|
||||
|
||||
ContentHint.IMPLICIT -> {
|
||||
Log.w(TAG, "${logPrefix(envelope)} The content hint is $contentHint, so no error message is needed.", true)
|
||||
Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSenderKeyDistributionMessage(envelope: Envelope, serviceId: ServiceId, deviceId: Int, message: SenderKeyDistributionMessage) {
|
||||
Log.i(TAG, "${logPrefix(envelope, serviceId)} Processing SenderKeyDistributionMessage")
|
||||
val sender = ApplicationDependencies.getSignalServiceMessageSender()
|
||||
sender.processSenderKeyDistributionMessage(SignalProtocolAddress(serviceId.toString(), deviceId), message)
|
||||
}
|
||||
|
||||
private fun handlePniSignatureMessage(envelope: Envelope, serviceId: ServiceId, e164: String?, deviceId: Int, pniSignatureMessage: PniSignatureMessage) {
|
||||
Log.i(TAG, "${logPrefix(envelope, serviceId)} Processing PniSignatureMessage")
|
||||
|
||||
val pni: PNI = PNI.parseOrThrow(pniSignatureMessage.pni.toByteArray())
|
||||
|
||||
if (SignalDatabase.recipients.isAssociated(serviceId, pni)) {
|
||||
Log.i(TAG, "${logPrefix(envelope, serviceId)}[handlePniSignatureMessage] ACI ($serviceId) and PNI ($pni) are already associated.")
|
||||
return
|
||||
}
|
||||
|
||||
val identityStore = ApplicationDependencies.getProtocolStore().aci().identities()
|
||||
val aciAddress = SignalProtocolAddress(serviceId.toString(), deviceId)
|
||||
val pniAddress = SignalProtocolAddress(pni.toString(), deviceId)
|
||||
val aciIdentity = identityStore.getIdentity(aciAddress)
|
||||
val pniIdentity = identityStore.getIdentity(pniAddress)
|
||||
|
||||
if (aciIdentity == null) {
|
||||
Log.w(TAG, "${logPrefix(envelope, serviceId)}[validatePniSignature] No identity found for ACI address $aciAddress")
|
||||
return
|
||||
}
|
||||
|
||||
if (pniIdentity == null) {
|
||||
Log.w(TAG, "${logPrefix(envelope, serviceId)}[validatePniSignature] No identity found for PNI address $pniAddress")
|
||||
return
|
||||
}
|
||||
|
||||
if (pniIdentity.verifyAlternateIdentity(aciIdentity, pniSignatureMessage.signature.toByteArray())) {
|
||||
Log.i(TAG, "${logPrefix(envelope, serviceId)}[validatePniSignature] PNI signature is valid. Associating ACI ($serviceId) with PNI ($pni)")
|
||||
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(serviceId, pni, e164)
|
||||
} else {
|
||||
Log.w(TAG, "${logPrefix(envelope, serviceId)}[validatePniSignature] Invalid PNI signature! Cannot associate ACI ($serviceId) with PNI ($pni)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun postErrorNotification(context: Context) {
|
||||
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.getString(R.string.MessageDecryptionUtil_failed_to_decrypt_message))
|
||||
.setContentText(context.getString(R.string.MessageDecryptionUtil_tap_to_send_a_debug_log))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
|
||||
}
|
||||
|
||||
private fun logPrefix(envelope: Envelope): String {
|
||||
return logPrefix(envelope.timestamp, envelope.sourceUuid ?: "<sealed>", envelope.sourceDevice)
|
||||
}
|
||||
|
||||
private fun logPrefix(envelope: Envelope, sender: ServiceId): String {
|
||||
return logPrefix(envelope.timestamp, sender.toString(), envelope.sourceDevice)
|
||||
}
|
||||
|
||||
private fun logPrefix(envelope: Envelope, cipherResult: SignalServiceCipherResult): String {
|
||||
return logPrefix(envelope.timestamp, cipherResult.metadata.sourceServiceId.toString(), envelope.sourceDevice)
|
||||
}
|
||||
|
||||
private fun logPrefix(envelope: Envelope, exception: ProtocolException): String {
|
||||
return if (exception.sender != null) {
|
||||
logPrefix(envelope.timestamp, exception.sender, exception.senderDevice)
|
||||
} else {
|
||||
logPrefix(envelope.timestamp, envelope.sourceUuid, envelope.sourceDevice)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPrefix(envelope: Envelope, exception: UnsupportedDataMessageException): String {
|
||||
return if (exception.sender != null) {
|
||||
logPrefix(envelope.timestamp, exception.sender, exception.senderDevice)
|
||||
} else {
|
||||
logPrefix(envelope.timestamp, envelope.sourceUuid, envelope.sourceDevice)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPrefix(timestamp: Long, sender: String?, deviceId: Int): String {
|
||||
val senderString = sender ?: "null"
|
||||
return "[$timestamp] $senderString:$deviceId |"
|
||||
}
|
||||
|
||||
private fun buildSendRetryReceiptJob(envelope: Envelope, protocolException: ProtocolException, sender: Recipient): SendRetryReceiptJob {
|
||||
val originalContent: ByteArray
|
||||
val envelopeType: Int
|
||||
|
||||
if (protocolException.unidentifiedSenderMessageContent.isPresent) {
|
||||
originalContent = protocolException.unidentifiedSenderMessageContent.get().content
|
||||
envelopeType = protocolException.unidentifiedSenderMessageContent.get().type
|
||||
} else {
|
||||
originalContent = envelope.content.toByteArray()
|
||||
envelopeType = envelope.type.number.toCiphertextMessageType()
|
||||
}
|
||||
|
||||
val decryptionErrorMessage: DecryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, envelopeType, envelope.timestamp, protocolException.senderDevice)
|
||||
val groupId: GroupId? = protocolException.parseGroupId(envelope)
|
||||
return SendRetryReceiptJob(sender.id, Optional.ofNullable(groupId), decryptionErrorMessage)
|
||||
}
|
||||
|
||||
private fun ProtocolException.parseGroupId(envelope: Envelope): GroupId? {
|
||||
return if (this.groupId.isPresent) {
|
||||
try {
|
||||
GroupId.push(this.groupId.get())
|
||||
} catch (e: BadGroupIdException) {
|
||||
Log.w(TAG, "[${envelope.timestamp}] Bad groupId!", true)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Envelope.getDestination(selfAci: ServiceId, selfPni: ServiceId): ServiceId {
|
||||
return if (!FeatureFlags.phoneNumberPrivacy()) {
|
||||
selfAci
|
||||
} else if (this.hasDestinationUuid()) {
|
||||
val serviceId = ServiceId.parseOrThrow(this.destinationUuid)
|
||||
if (serviceId == selfAci || serviceId == selfPni) {
|
||||
serviceId
|
||||
} else {
|
||||
Log.w(TAG, "Destination of $serviceId does not match our ACI ($selfAci) or PNI ($selfPni)! Defaulting to ACI.")
|
||||
selfAci
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No destinationUuid set! Defaulting to ACI.")
|
||||
selfAci
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toCiphertextMessageType(): Int {
|
||||
return when (this) {
|
||||
Envelope.Type.CIPHERTEXT_VALUE -> CiphertextMessage.WHISPER_TYPE
|
||||
Envelope.Type.PREKEY_BUNDLE_VALUE -> CiphertextMessage.PREKEY_TYPE
|
||||
Envelope.Type.UNIDENTIFIED_SENDER_VALUE -> CiphertextMessage.SENDERKEY_TYPE
|
||||
Envelope.Type.PLAINTEXT_CONTENT_VALUE -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE
|
||||
else -> CiphertextMessage.WHISPER_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProtocolException.toErrorMetadata(): ErrorMetadata {
|
||||
return ErrorMetadata(
|
||||
sender = this.sender,
|
||||
senderDevice = this.senderDevice,
|
||||
groupId = if (this.groupId.isPresent) GroupId.v2(GroupMasterKey(this.groupId.get())) else null
|
||||
)
|
||||
}
|
||||
|
||||
private fun SignalServiceCipherResult.toErrorMetadata(): ErrorMetadata {
|
||||
return ErrorMetadata(
|
||||
sender = this.metadata.sourceServiceId.toString(),
|
||||
senderDevice = this.metadata.sourceDeviceId,
|
||||
groupId = null
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
val envelope: Envelope
|
||||
val serverDeliveredTimestamp: Long
|
||||
val followUpOperations: List<Runnable>
|
||||
|
||||
/** Successfully decrypted the envelope content. The plaintext [Content] is available. */
|
||||
class Success(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
val content: Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result
|
||||
|
||||
/** We could not decrypt the message, and an error should be inserted into the user's chat history. */
|
||||
class DecryptionError(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
override val errorMetadata: ErrorMetadata,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result, Error
|
||||
|
||||
/** The envelope used an invalid version of the Signal protocol. */
|
||||
class InvalidVersion(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
override val errorMetadata: ErrorMetadata,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result, Error
|
||||
|
||||
/** The envelope used an old format that hasn't been used since 2015. This shouldn't be happening. */
|
||||
class LegacyMessage(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
override val errorMetadata: ErrorMetadata,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result, Error
|
||||
|
||||
/**
|
||||
* Indicates the that the [org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.getRequiredProtocolVersion]
|
||||
* is higher than we support.
|
||||
*/
|
||||
class UnsupportedDataMessage(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
override val errorMetadata: ErrorMetadata,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result, Error
|
||||
|
||||
/** There are no further results from this envelope that need to be processed. There may still be [followUpOperations]. */
|
||||
class Ignore(
|
||||
override val envelope: Envelope,
|
||||
override val serverDeliveredTimestamp: Long,
|
||||
override val followUpOperations: List<Runnable>
|
||||
) : Result
|
||||
|
||||
interface Error {
|
||||
val errorMetadata: ErrorMetadata
|
||||
}
|
||||
}
|
||||
|
||||
data class ErrorMetadata(
|
||||
val sender: String,
|
||||
val senderDevice: Int,
|
||||
val groupId: GroupId?
|
||||
)
|
||||
}
|
|
@ -271,7 +271,7 @@ public class LegacyMigrationJob extends MigrationJob {
|
|||
try (PushTable.Reader pushReader = pushDatabase.readerFor(pushDatabase.getPending())) {
|
||||
SignalServiceEnvelope envelope;
|
||||
while ((envelope = pushReader.getNext()) != null) {
|
||||
jobManager.add(new PushDecryptMessageJob(context, envelope));
|
||||
jobManager.add(new PushDecryptMessageJob(envelope));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package org.whispersystems.signalservice.api.crypto
|
||||
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
|
||||
class EnvelopeMetadata(
|
||||
val sourceServiceId: ServiceId,
|
||||
val sourceE164: String?,
|
||||
val sourceDeviceId: Int,
|
||||
val sealedSender: Boolean,
|
||||
val groupId: ByteArray?,
|
||||
val destinationServiceId: ServiceId
|
||||
)
|
|
@ -49,10 +49,12 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer;
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer;
|
||||
|
@ -144,7 +146,7 @@ public class SignalServiceCipher {
|
|||
{
|
||||
try {
|
||||
if (envelope.hasContent()) {
|
||||
Plaintext plaintext = decrypt(envelope, envelope.getContent());
|
||||
Plaintext plaintext = decryptInternal(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData());
|
||||
|
||||
SignalServiceContentProto contentProto = SignalServiceContentProto.newBuilder()
|
||||
|
@ -162,7 +164,39 @@ public class SignalServiceCipher {
|
|||
}
|
||||
}
|
||||
|
||||
private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext)
|
||||
public SignalServiceCipherResult decrypt(Envelope envelope, long serverDeliveredTimestamp)
|
||||
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
|
||||
ProtocolInvalidKeyIdException, ProtocolLegacyMessageException,
|
||||
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
|
||||
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
|
||||
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
|
||||
SelfSendException, InvalidMessageStructureException
|
||||
{
|
||||
try {
|
||||
if (envelope.hasContent()) {
|
||||
Plaintext plaintext = decryptInternal(envelope, serverDeliveredTimestamp);
|
||||
SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData());
|
||||
|
||||
return new SignalServiceCipherResult(
|
||||
content,
|
||||
new EnvelopeMetadata(
|
||||
plaintext.metadata.getSender().getServiceId(),
|
||||
plaintext.metadata.getSender().getNumber().orElse(null),
|
||||
plaintext.metadata.getSenderDevice(),
|
||||
plaintext.metadata.isNeedsReceipt(),
|
||||
plaintext.metadata.getGroupId().orElse(null),
|
||||
localAddress.getServiceId()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMetadataMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Plaintext decryptInternal(Envelope envelope, long serverDeliveredTimestamp)
|
||||
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
|
||||
ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException,
|
||||
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
|
||||
|
@ -175,30 +209,30 @@ public class SignalServiceCipher {
|
|||
byte[] paddedMessage;
|
||||
SignalServiceMetadata metadata;
|
||||
|
||||
if (!envelope.hasSourceUuid() && !envelope.isUnidentifiedSender()) {
|
||||
if (!envelope.hasSourceUuid() && envelope.getType().getNumber() != Envelope.Type.UNIDENTIFIED_SENDER_VALUE) {
|
||||
throw new InvalidMessageStructureException("Non-UD envelope is missing a UUID!");
|
||||
}
|
||||
|
||||
if (envelope.isPreKeySignalMessage()) {
|
||||
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid().get(), envelope.getSourceDevice());
|
||||
if (envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE) {
|
||||
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
|
||||
|
||||
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(envelope.getContent().toByteArray()));
|
||||
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
|
||||
signalProtocolStore.clearSenderKeySharedWith(Collections.singleton(sourceAddress));
|
||||
} else if (envelope.isSignalMessage()) {
|
||||
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid().get(), envelope.getSourceDevice());
|
||||
} else if (envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE) {
|
||||
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
|
||||
|
||||
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
} else if (envelope.isPlaintextContent()) {
|
||||
paddedMessage = new PlaintextContent(ciphertext).getBody();
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
} else if (envelope.isUnidentifiedSender()) {
|
||||
paddedMessage = sessionCipher.decrypt(new SignalMessage(envelope.getContent().toByteArray()));
|
||||
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
} else if (envelope.getType().getNumber() == Envelope.Type.PLAINTEXT_CONTENT_VALUE) {
|
||||
paddedMessage = new PlaintextContent(envelope.getContent().toByteArray()).getBody();
|
||||
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
|
||||
} else if (envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE) {
|
||||
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().uuid(), localAddress.getNumber().orElse(null), localDeviceId));
|
||||
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp());
|
||||
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, envelope.getContent().toByteArray(), envelope.getServerTimestamp());
|
||||
SignalServiceAddress resultAddress = new SignalServiceAddress(ACI.parseOrThrow(result.getSenderUuid()), result.getSenderE164());
|
||||
Optional<byte[]> groupId = result.getGroupId();
|
||||
boolean needsReceipt = true;
|
||||
|
@ -213,7 +247,7 @@ public class SignalServiceCipher {
|
|||
}
|
||||
|
||||
paddedMessage = result.getPaddedMessage();
|
||||
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), needsReceipt, envelope.getServerGuid(), groupId, envelope.getDestinationUuid());
|
||||
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, needsReceipt, envelope.getServerGuid(), groupId, envelope.getDestinationUuid());
|
||||
} else {
|
||||
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||
}
|
||||
|
@ -223,24 +257,28 @@ public class SignalServiceCipher {
|
|||
|
||||
return new Plaintext(metadata, data);
|
||||
} catch (DuplicateMessageException e) {
|
||||
throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolDuplicateMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (LegacyMessageException e) {
|
||||
throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolLegacyMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolInvalidMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolInvalidKeyException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (UntrustedIdentityException e) {
|
||||
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (InvalidVersionException e) {
|
||||
throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolInvalidVersionException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
} catch (NoSessionException e) {
|
||||
throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||
throw new ProtocolNoSessionException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
|
||||
}
|
||||
}
|
||||
|
||||
private static SignalServiceAddress getSourceAddress(Envelope envelope) {
|
||||
return new SignalServiceAddress(ServiceId.parseOrNull(envelope.getSourceUuid()));
|
||||
}
|
||||
|
||||
private static class Plaintext {
|
||||
private final SignalServiceMetadata metadata;
|
||||
private final byte[] data;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package org.whispersystems.signalservice.api.crypto
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
/**
|
||||
* Represents the output of decrypting a [SignalServiceProtos.Envelope] via [SignalServiceCipher.decrypt]
|
||||
*
|
||||
* @param content The [SignalServiceProtos.Content] that was decrypted from the envelope.
|
||||
* @param metadata The decrypted metadata of the envelope. Represents sender information that may have
|
||||
* been encrypted with sealed sender.
|
||||
*/
|
||||
data class SignalServiceCipherResult(
|
||||
val content: SignalServiceProtos.Content,
|
||||
val metadata: EnvelopeMetadata
|
||||
)
|
|
@ -0,0 +1,270 @@
|
|||
package org.whispersystems.signalservice.api.messages
|
||||
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.StoryMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage
|
||||
|
||||
/**
|
||||
* Validates an [Envelope] and its decrypted [Content] so that we know the message can be processed safely
|
||||
* down the line.
|
||||
*
|
||||
* Mostly makes sure that UUIDs are valid, required fields are presents, etc.
|
||||
*/
|
||||
object EnvelopeContentValidator {
|
||||
|
||||
fun validate(envelope: Envelope, content: Content): Result {
|
||||
return when {
|
||||
envelope.story && !content.meetsStoryFlagCriteria() -> Result.Invalid("Envelope was flagged as a story, but it did not have any story-related content!")
|
||||
content.hasDataMessage() -> validateDataMessage(envelope, content.dataMessage)
|
||||
content.hasSyncMessage() -> validateSyncMessage(envelope, content.syncMessage)
|
||||
content.hasCallMessage() -> Result.Valid
|
||||
content.hasReceiptMessage() -> validateReceiptMessage(content.receiptMessage)
|
||||
content.hasTypingMessage() -> validateTypingMessage(envelope, content.typingMessage)
|
||||
content.hasDecryptionErrorMessage() -> validateDecryptionErrorMessage(content.decryptionErrorMessage.toByteArray())
|
||||
content.hasStoryMessage() -> validateStoryMessage(content.storyMessage)
|
||||
content.hasPniSignatureMessage() -> Result.Valid
|
||||
content.hasSenderKeyDistributionMessage() -> Result.Valid
|
||||
else -> Result.Invalid("Content is empty!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDataMessage(envelope: Envelope, dataMessage: DataMessage): Result {
|
||||
if (dataMessage.requiredProtocolVersion > DataMessage.ProtocolVersion.CURRENT_VALUE) {
|
||||
return Result.UnsupportedDataMessage(
|
||||
ourVersion = DataMessage.ProtocolVersion.CURRENT_VALUE,
|
||||
theirVersion = dataMessage.requiredProtocolVersion
|
||||
)
|
||||
}
|
||||
|
||||
if (!dataMessage.hasTimestamp()) {
|
||||
return Result.Invalid("[DataMessage] Missing timestamp!")
|
||||
}
|
||||
|
||||
if (dataMessage.timestamp != envelope.timestamp) {
|
||||
Result.Invalid("[DataMessage] Timestamps don't match! envelope: ${envelope.timestamp}, content: ${dataMessage.timestamp}")
|
||||
}
|
||||
|
||||
if (dataMessage.hasQuote() && dataMessage.quote.authorUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[DataMessage] Invalid UUID on quote!")
|
||||
}
|
||||
|
||||
if (dataMessage.contactList.any { it.hasAvatar() && it.avatar.avatar.isPresentAndInvalid() }) {
|
||||
return Result.Invalid("[DataMessage] Invalid AttachmentPointer on DataMessage.contactList.avatar!")
|
||||
}
|
||||
|
||||
if (dataMessage.previewList.any { it.hasImage() && it.image.isPresentAndInvalid() }) {
|
||||
return Result.Invalid("[DataMessage] Invalid AttachmentPointer on DataMessage.previewList.image!")
|
||||
}
|
||||
|
||||
if (dataMessage.bodyRangesList.any { it.hasMentionUuid() && it.mentionUuid.isNullOrInvalidUuid() }) {
|
||||
return Result.Invalid("[DataMessage] Invalid UUID on body range!")
|
||||
}
|
||||
|
||||
if (dataMessage.hasSticker() && dataMessage.sticker.data.isNullOrInvalid()) {
|
||||
return Result.Invalid("[DataMessage] Invalid AttachmentPointer on DataMessage.sticker!")
|
||||
}
|
||||
|
||||
if (dataMessage.hasReaction()) {
|
||||
if (!dataMessage.reaction.hasTargetSentTimestamp()) {
|
||||
return Result.Invalid("[DataMessage] Missing timestamp on DataMessage.reaction!")
|
||||
}
|
||||
if (dataMessage.reaction.targetAuthorUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[DataMessage] Invalid UUID on DataMessage.reaction!")
|
||||
}
|
||||
}
|
||||
|
||||
if (dataMessage.hasDelete() && !dataMessage.delete.hasTargetSentTimestamp()) {
|
||||
return Result.Invalid("[DataMessage] Missing timestamp on DataMessage.delete!")
|
||||
}
|
||||
|
||||
if (dataMessage.hasStoryContext() && dataMessage.storyContext.authorUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[DataMessage] Invalid UUID on DataMessage.storyContext!")
|
||||
}
|
||||
|
||||
if (dataMessage.hasGiftBadge()) {
|
||||
if (!dataMessage.giftBadge.hasReceiptCredentialPresentation()) {
|
||||
return Result.Invalid("[DataMessage] Missing DataMessage.giftBadge.receiptCredentialPresentation!")
|
||||
}
|
||||
if (!dataMessage.giftBadge.hasReceiptCredentialPresentation()) {
|
||||
try {
|
||||
ReceiptCredentialPresentation(dataMessage.giftBadge.receiptCredentialPresentation.toByteArray())
|
||||
} catch (e: InvalidInputException) {
|
||||
return Result.Invalid("[DataMessage] Invalid DataMessage.giftBadge.receiptCredentialPresentation!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dataMessage.attachmentsList.any { it.isNullOrInvalid() }) {
|
||||
return Result.Invalid("[DataMessage] Invalid attachments!")
|
||||
}
|
||||
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun validateSyncMessage(envelope: Envelope, syncMessage: SyncMessage): Result {
|
||||
if (syncMessage.hasSent()) {
|
||||
val validAddress = syncMessage.sent.destinationUuid.isValidUuid()
|
||||
val hasDataGroup = syncMessage.sent.message?.hasGroupV2() ?: false
|
||||
val hasStoryGroup = syncMessage.sent.storyMessage?.hasGroup() ?: false
|
||||
val hasStoryManifest = syncMessage.sent.storyMessageRecipientsList.isNotEmpty()
|
||||
|
||||
if (hasDataGroup) {
|
||||
validateGroupContextV2(syncMessage.sent.message.groupV2, "[SyncMessage.Sent.Message]")?.let { return it }
|
||||
}
|
||||
|
||||
if (hasStoryGroup) {
|
||||
validateGroupContextV2(syncMessage.sent.storyMessage.group, "[SyncMessage.Sent.StoryMessage]")?.let { return it }
|
||||
}
|
||||
|
||||
if (!validAddress && !hasDataGroup && !hasStoryGroup && !hasStoryManifest) {
|
||||
return Result.Invalid("[SyncMessage] No valid destination! Checked the destination, DataMessage.group, StoryMessage.group, and storyMessageRecipientList")
|
||||
}
|
||||
|
||||
for (status in syncMessage.sent.unidentifiedStatusList) {
|
||||
if (status.destinationUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.sent.unidentifiedStatusList!")
|
||||
}
|
||||
}
|
||||
|
||||
return if (syncMessage.sent.hasMessage()) {
|
||||
validateDataMessage(envelope, syncMessage.sent.message)
|
||||
} else if (syncMessage.sent.hasStoryMessage()) {
|
||||
validateStoryMessage(syncMessage.sent.storyMessage)
|
||||
} else {
|
||||
Result.Invalid("[SyncMessage] Empty SyncMessage.sent!")
|
||||
}
|
||||
}
|
||||
|
||||
if (syncMessage.readList.any { it.senderUuid.isNullOrInvalidUuid() }) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.readList!")
|
||||
}
|
||||
|
||||
if (syncMessage.viewedList.any { it.senderUuid.isNullOrInvalidUuid() }) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.viewList!")
|
||||
}
|
||||
|
||||
if (syncMessage.hasViewOnceOpen() && syncMessage.viewOnceOpen.senderUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.viewOnceOpen!")
|
||||
}
|
||||
|
||||
if (syncMessage.hasVerified() && syncMessage.verified.destinationUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.verified!")
|
||||
}
|
||||
|
||||
if (syncMessage.stickerPackOperationList.any { !it.hasPackId() }) {
|
||||
return Result.Invalid("[SyncMessage] Missing packId in stickerPackOperationList!")
|
||||
}
|
||||
|
||||
if (syncMessage.hasBlocked() && syncMessage.blocked.uuidsList.any { it.isNullOrInvalidUuid() }) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.blocked!")
|
||||
}
|
||||
|
||||
if (syncMessage.hasMessageRequestResponse() && !syncMessage.messageRequestResponse.hasGroupId() && syncMessage.messageRequestResponse.threadUuid.isNullOrInvalidUuid()) {
|
||||
return Result.Invalid("[SyncMessage] Invalid UUID in SyncMessage.messageRequestResponse!")
|
||||
}
|
||||
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun validateReceiptMessage(receiptMessage: ReceiptMessage): Result {
|
||||
return if (!receiptMessage.hasType()) {
|
||||
Result.Invalid("[ReceiptMessage] Missing type!")
|
||||
} else {
|
||||
Result.Valid
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateTypingMessage(envelope: Envelope, typingMessage: TypingMessage): Result {
|
||||
return if (!typingMessage.hasTimestamp()) {
|
||||
return Result.Invalid("[TypingMessage] Missing timestamp!")
|
||||
} else if (typingMessage.hasTimestamp() && typingMessage.timestamp != envelope.timestamp) {
|
||||
Result.Invalid("[TypingMessage] Timestamps don't match! envelope: ${envelope.timestamp}, content: ${typingMessage.timestamp}")
|
||||
} else if (!typingMessage.hasAction()) {
|
||||
Result.Invalid("[TypingMessage] Missing action!")
|
||||
} else {
|
||||
Result.Valid
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDecryptionErrorMessage(serializedDecryptionErrorMessage: ByteArray): Result {
|
||||
return try {
|
||||
DecryptionErrorMessage(serializedDecryptionErrorMessage)
|
||||
Result.Valid
|
||||
} catch (e: InvalidMessageStructureException) {
|
||||
Result.Invalid("[DecryptionErrorMessage] Bad decryption error message!", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateStoryMessage(storyMessage: StoryMessage): Result {
|
||||
if (storyMessage.hasGroup()) {
|
||||
validateGroupContextV2(storyMessage.group, "[StoryMessage]")?.let { return it }
|
||||
}
|
||||
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun AttachmentPointer?.isNullOrInvalid(): Boolean {
|
||||
return this == null || this.attachmentIdentifierCase == AttachmentPointer.AttachmentIdentifierCase.ATTACHMENTIDENTIFIER_NOT_SET
|
||||
}
|
||||
|
||||
private fun AttachmentPointer?.isPresentAndInvalid(): Boolean {
|
||||
return this != null && this.attachmentIdentifierCase == AttachmentPointer.AttachmentIdentifierCase.ATTACHMENTIDENTIFIER_NOT_SET
|
||||
}
|
||||
|
||||
private fun String?.isValidUuid(): Boolean {
|
||||
return UuidUtil.isUuid(this)
|
||||
}
|
||||
|
||||
private fun String?.isNullOrInvalidUuid(): Boolean {
|
||||
return !UuidUtil.isUuid(this)
|
||||
}
|
||||
|
||||
private fun Content?.meetsStoryFlagCriteria(): Boolean {
|
||||
return when {
|
||||
this == null -> false
|
||||
this.hasSenderKeyDistributionMessage() -> true
|
||||
this.hasStoryMessage() -> true
|
||||
this.hasDataMessage() && this.dataMessage.hasStoryContext() && this.dataMessage.hasGroupV2() -> true
|
||||
this.hasDataMessage() && this.dataMessage.hasDelete() -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateGroupContextV2(groupContext: GroupContextV2, prefix: String): Result.Invalid? {
|
||||
return if (!groupContext.hasMasterKey()) {
|
||||
Result.Invalid("$prefix Missing GV2 master key!")
|
||||
} else if (!groupContext.hasRevision()) {
|
||||
Result.Invalid("$prefix Missing GV2 revision!")
|
||||
} else {
|
||||
try {
|
||||
GroupMasterKey(groupContext.masterKey.toByteArray())
|
||||
null
|
||||
} catch (e: InvalidInputException) {
|
||||
Result.Invalid("$prefix Bad GV2 master key!", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
/** Content is valid. */
|
||||
object Valid : Result()
|
||||
|
||||
/** The [DataMessage.requiredProtocolVersion_] is newer than the one we support. */
|
||||
class UnsupportedDataMessage(val ourVersion: Int, val theirVersion: Int) : Result()
|
||||
|
||||
/** The contents of the proto do not match our expectations, e.g. invalid UUIDs, missing required fields, etc. */
|
||||
class Invalid(val reason: String, val throwable: Throwable = Throwable()) : Result()
|
||||
}
|
||||
}
|
|
@ -271,6 +271,10 @@ public class SignalServiceEnvelope {
|
|||
return envelope.getReportingToken().toByteArray();
|
||||
}
|
||||
|
||||
public Envelope getProto() {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private SignalServiceEnvelopeProto.Builder serializeToProto() {
|
||||
SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder()
|
||||
.setType(getType())
|
||||
|
|
|
@ -23,6 +23,10 @@ public final class PNI extends ServiceId {
|
|||
return from(UUID.fromString(raw));
|
||||
}
|
||||
|
||||
public static PNI parseOrThrow(byte[] raw) {
|
||||
return from(UuidUtil.parseOrThrow(raw));
|
||||
}
|
||||
|
||||
public static PNI parseOrNull(byte[] raw) {
|
||||
UUID uuid = UuidUtil.parseOrNull(raw);
|
||||
return uuid != null ? from(uuid) : null;
|
||||
|
|
Ładowanie…
Reference in New Issue