From 6d6e017c7133b6670e0881ed377c2570afa0cda5 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 16 Apr 2020 09:30:46 -0300 Subject: [PATCH] Proactively share profile key after accepting a message request. --- .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/ProfileKeySendJob.java | 161 ++++++++++++++++++ .../MessageRequestRepository.java | 2 + .../securesms/sms/MessageSender.java | 7 + 4 files changed, 171 insertions(+) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index c73d84f105..dbfca4cbff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -74,6 +74,7 @@ public final class JobManagerFactories { put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory()); put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory()); put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory()); + put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); put(PushDecryptMessageJob.KEY, new PushDecryptMessageJob.Factory()); put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java new file mode 100644 index 0000000000..91f55787d9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -0,0 +1,161 @@ +package org.thoughtcrime.securesms.jobs; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class ProfileKeySendJob extends BaseJob { + + private static final String TAG = Log.tag(ProfileKeySendJob.class); + private static final String KEY_RECIPIENTS = "recipients"; + private static final String KEY_THREAD = "thread"; + + public static final String KEY = "ProfileKeySendJob"; + + private final long threadId; + private final List recipients; + + @WorkerThread + public static ProfileKeySendJob create(@NonNull Context context, long threadId) { + Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + + if (conversationRecipient == null) { + throw new AssertionError("We have a thread but no recipient!"); + } + + List recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList() + : Stream.of(conversationRecipient.getId()).toList(); + + recipients.remove(Recipient.self().getId()); + + return new ProfileKeySendJob(new Parameters.Builder() + .setQueue(conversationRecipient.getId().toQueueKey()) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), threadId, recipients); + } + + private ProfileKeySendJob(@NonNull Parameters parameters, long threadId, @NonNull List recipients) { + super(parameters); + this.threadId = threadId; + this.recipients = recipients; + } + + @Override + protected void onRun() throws Exception { + Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + + if (conversationRecipient == null) { + throw new AssertionError("We have a thread but no recipient!"); + } + + List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); + List completions = deliver(conversationRecipient, destinations); + + for (Recipient completion : completions) { + recipients.remove(completion.getId()); + } + + Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size()); + + if (!recipients.isEmpty()) { + Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying."); + throw new RetryLaterException(); + } + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof IOException || + e instanceof RetryLaterException; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder() + .putLong(KEY_THREAD, threadId) + .putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients)) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onFailure() { + + } + + private List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations) throws IOException, UntrustedIdentityException { + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + List addresses = Stream.of(destinations).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList(); + List> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList(); + SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(System.currentTimeMillis()) + .withProfileKey(Recipient.self().resolve().getProfileKey()); + + if (conversationRecipient.isGroup()) { + dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId())); + } + + List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + + Stream.of(results) + .filter(r -> r.getIdentityFailure() != null) + .map(SendMessageResult::getAddress) + .map(a -> Recipient.externalPush(context, a)) + .forEach(r -> Log.w(TAG, "Identity failure for " + r.getId())); + + Stream.of(results) + .filter(SendMessageResult::isUnregisteredFailure) + .map(SendMessageResult::getAddress) + .map(a -> Recipient.externalPush(context, a)) + .forEach(r -> Log.w(TAG, "Unregistered failure for " + r.getId())); + + + return Stream.of(results) + .filter(r -> r.getSuccess() != null || r.getIdentityFailure() != null || r.isUnregisteredFailure()) + .map(SendMessageResult::getAddress) + .map(a -> Recipient.externalPush(context, a)) + .toList(); + } + + public static class Factory implements Job.Factory { + + @Override + public @NonNull ProfileKeySendJob create(@NonNull Parameters parameters, @NonNull Data data) { + long threadId = data.getLong(KEY_THREAD); + List recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS)); + + return new ProfileKeySendJob(parameters, threadId, recipients); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index 03d5a7b2c7..da0cbdb851 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Optional; @@ -79,6 +80,7 @@ final class MessageRequestRepository { ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId())); } + MessageSender.sendProfileKey(context, threadId); onMessageRequestAccepted.run(); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 882891998d..9adc5b1999 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -22,6 +22,7 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; @@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.jobs.AttachmentCopyJob; import org.thoughtcrime.securesms.jobs.AttachmentMarkUploadedJob; import org.thoughtcrime.securesms.jobs.AttachmentUploadJob; import org.thoughtcrime.securesms.jobs.MmsSendJob; +import org.thoughtcrime.securesms.jobs.ProfileKeySendJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob; import org.thoughtcrime.securesms.jobs.PushTextSendJob; @@ -80,6 +82,11 @@ public class MessageSender { private static final String TAG = MessageSender.class.getSimpleName(); + @WorkerThread + public static void sendProfileKey(final Context context, final long threadId) { + ApplicationDependencies.getJobManager().add(ProfileKeySendJob.create(context, threadId)); + } + public static long send(final Context context, final OutgoingTextMessage message, final long threadId,