diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java deleted file mode 100644 index 9eabeb7d4b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ /dev/null @@ -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 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 { - @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)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt new file mode 100644 index 0000000000..b732d5d597 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt @@ -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 { + override fun create(parameters: Parameters, data: Data): PushDecryptMessageJob { + return PushDecryptMessageJob( + parameters, + SignalServiceEnvelope.deserialize(data.getStringAsBlob(KEY_ENVELOPE)), + data.getLong(KEY_SMS_MESSAGE_ID) + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 48f1f111ac..73dbe71d86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java index f425e00c5d..07e447c49f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 1c06a8c719..5c4cbc99f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -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 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java deleted file mode 100644 index d033d35ecb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java +++ /dev/null @@ -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 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 = 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 jobs; - - static @NonNull DecryptionResult forSuccess(@NonNull SignalServiceContent content, @NonNull List jobs) { - return new DecryptionResult(MessageState.DECRYPTED_OK, content, null, jobs); - } - - static @NonNull DecryptionResult forError(@NonNull MessageState messageState, - @NonNull ExceptionMetadata exception, - @NonNull List jobs) - { - return new DecryptionResult(messageState, null, exception, jobs); - } - - static @NonNull DecryptionResult forNoop(@NonNull List jobs) { - return new DecryptionResult(MessageState.NOOP, null, null, jobs); - } - - private DecryptionResult(@NonNull MessageState state, - @Nullable SignalServiceContent content, - @Nullable ExceptionMetadata exception, - @NonNull List 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 getJobs() { - return jobs; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt new file mode 100644 index 0000000000..79b1b3c9ef --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -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 = 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, + 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 ?: "", 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 + + /** 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 + ) : 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 + ) : 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 + ) : 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 + ) : 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 + ) : 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 + ) : Result + + interface Error { + val errorMetadata: ErrorMetadata + } + } + + data class ErrorMetadata( + val sender: String, + val senderDevice: Int, + val groupId: GroupId? + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index fa7632b751..7c49e1cbc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -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)); } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeMetadata.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeMetadata.kt new file mode 100644 index 0000000000..91924af8c7 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeMetadata.kt @@ -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 +) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index ddda1d1294..6bf64dd5aa 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -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 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; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipherResult.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipherResult.kt new file mode 100644 index 0000000000..c496f4e8c5 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipherResult.kt @@ -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 +) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt new file mode 100644 index 0000000000..cd361c7bf1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt @@ -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() + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 453aa44fdc..cf2cd193a7 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -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()) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java index a524005909..407791b033 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java @@ -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;