kopia lustrzana https://github.com/ryukoposting/Signal-Android
309 wiersze
15 KiB
Java
309 wiersze
15 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|