Initial refactor of the message decryption flow.

main
Greyson Parrelli 2023-03-03 01:10:33 -05:00
rodzic c1a94be9cd
commit ec2565263e
14 zmienionych plików z 1077 dodań i 696 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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?
)
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -49,10 +49,12 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer;
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer;
@ -144,7 +146,7 @@ public class SignalServiceCipher {
{
try {
if (envelope.hasContent()) {
Plaintext plaintext = decrypt(envelope, envelope.getContent());
Plaintext plaintext = decryptInternal(envelope.getProto(), envelope.getServerDeliveredTimestamp());
SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData());
SignalServiceContentProto contentProto = SignalServiceContentProto.newBuilder()
@ -162,7 +164,39 @@ public class SignalServiceCipher {
}
}
private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext)
public SignalServiceCipherResult decrypt(Envelope envelope, long serverDeliveredTimestamp)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolInvalidKeyIdException, ProtocolLegacyMessageException,
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
SelfSendException, InvalidMessageStructureException
{
try {
if (envelope.hasContent()) {
Plaintext plaintext = decryptInternal(envelope, serverDeliveredTimestamp);
SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData());
return new SignalServiceCipherResult(
content,
new EnvelopeMetadata(
plaintext.metadata.getSender().getServiceId(),
plaintext.metadata.getSender().getNumber().orElse(null),
plaintext.metadata.getSenderDevice(),
plaintext.metadata.isNeedsReceipt(),
plaintext.metadata.getGroupId().orElse(null),
localAddress.getServiceId()
)
);
} else {
return null;
}
} catch (InvalidProtocolBufferException e) {
throw new InvalidMetadataMessageException(e);
}
}
private Plaintext decryptInternal(Envelope envelope, long serverDeliveredTimestamp)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException,
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
@ -175,30 +209,30 @@ public class SignalServiceCipher {
byte[] paddedMessage;
SignalServiceMetadata metadata;
if (!envelope.hasSourceUuid() && !envelope.isUnidentifiedSender()) {
if (!envelope.hasSourceUuid() && envelope.getType().getNumber() != Envelope.Type.UNIDENTIFIED_SENDER_VALUE) {
throw new InvalidMessageStructureException("Non-UD envelope is missing a UUID!");
}
if (envelope.isPreKeySignalMessage()) {
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid().get(), envelope.getSourceDevice());
if (envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE) {
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid(), envelope.getSourceDevice());
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(envelope.getContent().toByteArray()));
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
signalProtocolStore.clearSenderKeySharedWith(Collections.singleton(sourceAddress));
} else if (envelope.isSignalMessage()) {
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid().get(), envelope.getSourceDevice());
} else if (envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE) {
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSourceUuid(), envelope.getSourceDevice());
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
} else if (envelope.isPlaintextContent()) {
paddedMessage = new PlaintextContent(ciphertext).getBody();
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
} else if (envelope.isUnidentifiedSender()) {
paddedMessage = sessionCipher.decrypt(new SignalMessage(envelope.getContent().toByteArray()));
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
} else if (envelope.getType().getNumber() == Envelope.Type.PLAINTEXT_CONTENT_VALUE) {
paddedMessage = new PlaintextContent(envelope.getContent().toByteArray()).getBody();
metadata = new SignalServiceMetadata(getSourceAddress(envelope), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, false, envelope.getServerGuid(), Optional.empty(), envelope.getDestinationUuid());
} else if (envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE) {
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().uuid(), localAddress.getNumber().orElse(null), localDeviceId));
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp());
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, envelope.getContent().toByteArray(), envelope.getServerTimestamp());
SignalServiceAddress resultAddress = new SignalServiceAddress(ACI.parseOrThrow(result.getSenderUuid()), result.getSenderE164());
Optional<byte[]> groupId = result.getGroupId();
boolean needsReceipt = true;
@ -213,7 +247,7 @@ public class SignalServiceCipher {
}
paddedMessage = result.getPaddedMessage();
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), needsReceipt, envelope.getServerGuid(), groupId, envelope.getDestinationUuid());
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerTimestamp(), serverDeliveredTimestamp, needsReceipt, envelope.getServerGuid(), groupId, envelope.getDestinationUuid());
} else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
}
@ -223,24 +257,28 @@ public class SignalServiceCipher {
return new Plaintext(metadata, data);
} catch (DuplicateMessageException e) {
throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolDuplicateMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (LegacyMessageException e) {
throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolLegacyMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (InvalidMessageException e) {
throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolInvalidMessageException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (InvalidKeyIdException e) {
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolInvalidKeyException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (UntrustedIdentityException e) {
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (InvalidVersionException e) {
throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolInvalidVersionException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
} catch (NoSessionException e) {
throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
throw new ProtocolNoSessionException(e, envelope.getSourceUuid(), envelope.getSourceDevice());
}
}
private static SignalServiceAddress getSourceAddress(Envelope envelope) {
return new SignalServiceAddress(ServiceId.parseOrNull(envelope.getSourceUuid()));
}
private static class Plaintext {
private final SignalServiceMetadata metadata;
private final byte[] data;

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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