mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 04:06:14 +00:00
Initial refactor of the message decryption flow.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user