Skip to content

Commit

Permalink
Add support for resending badly-encrypted stories.
Browse files Browse the repository at this point in the history
  • Loading branch information
greyson-signal authored and alex-signal committed Aug 18, 2022
1 parent 7873ec2 commit c6be427
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 71 deletions.
Expand Up @@ -320,7 +320,19 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}

fun getList(listId: DistributionListId): DistributionListRecord? {
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return getListByQuery("${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId))
}

fun getList(recipientId: RecipientId): DistributionListRecord? {
return getListByQuery("${ListTable.RECIPIENT_ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(recipientId))
}

fun getListByDistributionId(distributionId: DistributionId): DistributionListRecord? {
return getListByQuery("${ListTable.DISTRIBUTION_ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(distributionId))
}

private fun getListByQuery(query: String, args: Array<String>): DistributionListRecord? {
readableDatabase.query(ListTable.TABLE_NAME, null, query, args, null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
Expand Down Expand Up @@ -393,6 +405,16 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}
}

fun getDistributionId(recipientId: RecipientId): DistributionId? {
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.DISTRIBUTION_ID), "${ListTable.RECIPIENT_ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(recipientId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
DistributionId.from(cursor.requireString(ListTable.DISTRIBUTION_ID))
} else {
null
}
}
}

fun getMembers(listId: DistributionListId): List<RecipientId> {
lateinit var privacyMode: DistributionListPrivacyMode
lateinit var rawMembers: List<RecipientId>
Expand Down
Expand Up @@ -44,7 +44,7 @@ public class JobManager implements ConstraintObserver.Notifier {

private static final String TAG = Log.tag(JobManager.class);

public static final int CURRENT_VERSION = 8;
public static final int CURRENT_VERSION = 9;

private final Application application;
private final Configuration configuration;
Expand Down
@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.jobmanager.migrations;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.JobMigration;
import org.thoughtcrime.securesms.jobs.FailingJob;

import java.util.Optional;

/**
* We removed the messageId property from the job data and replaced it with a serialized envelope,
* so we need to take jobs that referenced an ID and replace it with the envelope instead.
*/
public class SenderKeyDistributionSendJobRecipientMigration extends JobMigration {

private static final String TAG = Log.tag(SenderKeyDistributionSendJobRecipientMigration.class);

private final GroupDatabase groupDatabase;

public SenderKeyDistributionSendJobRecipientMigration() {
this(SignalDatabase.groups());
}

@VisibleForTesting
SenderKeyDistributionSendJobRecipientMigration(GroupDatabase groupDatabase) {
super(9);
this.groupDatabase = groupDatabase;
}

@Override
protected @NonNull JobData migrate(@NonNull JobData jobData) {
if ("SenderKeyDistributionSendJob".equals(jobData.getFactoryKey())) {
return migrateJob(jobData, groupDatabase);
} else {
return jobData;
}
}

private static @NonNull JobData migrateJob(@NonNull JobData jobData, @NonNull GroupDatabase groupDatabase) {
Data data = jobData.getData();

if (data.hasString("group_id")) {
GroupId groupId = GroupId.pushOrThrow(data.getStringAsBlob("group_id"));
Optional<GroupRecord> group = groupDatabase.getGroup(groupId);

if (group.isPresent()) {
return jobData.withData(data.buildUpon()
.putString("thread_recipient_id", group.get().getRecipientId().serialize())
.build());

} else {
return jobData.withFactoryKey(FailingJob.KEY);
}
} else if (!data.hasString("thread_recipient_id")) {
return jobData.withFactoryKey(FailingJob.KEY);
} else {
return jobData;
}
}
}
Expand Up @@ -29,6 +29,7 @@
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SenderKeyDistributionSendJobRecipientMigration;
import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob;
import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJob;
import org.thoughtcrime.securesms.migrations.AttachmentCleanupMigrationJob;
Expand Down Expand Up @@ -272,6 +273,7 @@ public static List<JobMigration> getJobMigrations(@NonNull Application applicati
new SendReadReceiptsJobMigration(SignalDatabase.mmsSms()),
new PushProcessMessageQueueJobMigration(application),
new RetrieveProfileJobMigration(),
new PushDecryptMessageJobEnvelopeMigration(application));
new PushDecryptMessageJobEnvelopeMigration(application),
new SenderKeyDistributionSendJobRecipientMigration());
}
}
Expand Up @@ -13,6 +13,7 @@
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
Expand Down Expand Up @@ -143,14 +144,28 @@ protected void onRun() throws Exception {
Content contentToSend = content;

if (distributionId != null) {
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroupByDistributionId(distributionId);

if (!groupRecord.isPresent()) {
Log.w(TAG, "Could not find a matching group for the distributionId! Skipping message send.");
return;
} else if (!groupRecord.get().getMembers().contains(recipientId)) {
Log.w(TAG, "The target user is no longer in the group! Skipping message send.");
return;
if (groupId != null) {
Log.d(TAG, "GroupId is present. Assuming this is a group message.");
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroupByDistributionId(distributionId);

if (!groupRecord.isPresent()) {
Log.w(TAG, "Could not find a matching group for the distributionId! Skipping message send.");
return;
} else if (!groupRecord.get().getMembers().contains(recipientId)) {
Log.w(TAG, "The target user is no longer in the group! Skipping message send.");
return;
}
} else {
Log.d(TAG, "GroupId is not present. Assuming this is a message for a distribution list.");
DistributionListRecord listRecord = SignalDatabase.distributionLists().getListByDistributionId(distributionId);

if (listRecord == null) {
Log.w(TAG, "Could not find a matching distribution list for the distributionId! Skipping message send.");
return;
} else if (!listRecord.getMembers().contains(recipientId)) {
Log.w(TAG, "The target user is no longer in the distribution list! Skipping message send.");
return;
}
}

SenderKeyDistributionMessage senderKeyDistributionMessage = messageSender.getOrCreateNewGroupSession(distributionId);
Expand Down
Expand Up @@ -6,8 +6,8 @@
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
Expand Down Expand Up @@ -39,33 +39,33 @@ public final class SenderKeyDistributionSendJob extends BaseJob {

public static final String KEY = "SenderKeyDistributionSendJob";

private static final String KEY_RECIPIENT_ID = "recipient_id";
private static final String KEY_GROUP_ID = "group_id";
private static final String KEY_TARGET_RECIPIENT_ID = "recipient_id";
private static final String KEY_THREAD_RECIPIENT_ID = "thread_recipient_id";

private final RecipientId recipientId;
private final GroupId.V2 groupId;
private final RecipientId targetRecipientId;
private final RecipientId threadRecipientId;

public SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId) {
this(recipientId, groupId, new Parameters.Builder()
.setQueue(recipientId.toQueueKey())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setMaxInstancesForQueue(1)
.build());
public SenderKeyDistributionSendJob(@NonNull RecipientId targetRecipientId, RecipientId threadRecipientId) {
this(targetRecipientId, threadRecipientId, new Parameters.Builder()
.setQueue(targetRecipientId.toQueueKey())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setMaxInstancesForQueue(1)
.build());
}

private SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId, @NonNull Parameters parameters) {
private SenderKeyDistributionSendJob(@NonNull RecipientId targetRecipientId, @NonNull RecipientId threadRecipientId, @NonNull Parameters parameters) {
super(parameters);

this.recipientId = recipientId;
this.groupId = groupId;
this.targetRecipientId = targetRecipientId;
this.threadRecipientId = threadRecipientId;
}

@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
.putBlobAsString(KEY_GROUP_ID, groupId.getDecodedId())
return new Data.Builder().putString(KEY_TARGET_RECIPIENT_ID, targetRecipientId.serialize())
.putString(KEY_THREAD_RECIPIENT_ID, threadRecipientId.serialize())
.build();
}

Expand All @@ -76,38 +76,62 @@ private SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull

@Override
protected void onRun() throws Exception {
GroupDatabase groupDatabase = SignalDatabase.groups();
Recipient targetRecipient = Recipient.resolved(targetRecipientId);
Recipient threadRecipient = Recipient.resolved(threadRecipientId);

if (!groupDatabase.isCurrentMember(groupId, recipientId)) {
Log.w(TAG, recipientId + " is no longer a member of " + groupId + "! Not sending.");
if (targetRecipient.getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
Log.w(TAG, targetRecipientId + " does not support sender key! Not sending.");
return;
}

Recipient recipient = Recipient.resolved(recipientId);
if (targetRecipient.isUnregistered()) {
Log.w(TAG, threadRecipient.getId() + " not registered!");
return;
}

GroupId.V2 groupId;
DistributionId distributionId;

if (threadRecipient.isPushV2Group()) {
groupId = threadRecipient.requireGroupId().requireV2();
distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId);
} else if (threadRecipient.isDistributionList()) {
groupId = null;
distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipientId);
} else {
warn(TAG, "Recipient is not a group or distribution list! Skipping.");
return;
}

if (recipient.getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
Log.w(TAG, recipientId + " does not support sender key! Not sending.");
if (distributionId == null) {
warn(TAG, "Failed to find a distributionId! Skipping.");
return;
}

if (recipient.isUnregistered()) {
Log.w(TAG, recipient.getId() + " not registered!");
if (groupId != null && !SignalDatabase.groups().isCurrentMember(groupId, targetRecipientId)) {
Log.w(TAG, targetRecipientId + " is no longer a member of " + groupId + "! Not sending.");
return;
} else if (groupId == null) {
DistributionListRecord listRecord = SignalDatabase.distributionLists().getList(threadRecipientId);

if (listRecord == null || !listRecord.getMembers().contains(targetRecipientId)) {
Log.w(TAG, targetRecipientId + " is no longer a member of the distribution list! Not sending.");
return;
}
}

SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, recipient));
DistributionId distributionId = groupDatabase.getOrCreateDistributionId(groupId);
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(recipient));
List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient));
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(targetRecipient));

SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.of(groupId.getDecodedId()), false).get(0);
SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false).get(0);

if (result.isSuccess()) {
List<SignalProtocolAddress> addresses = result.getSuccess()
.getDevices()
.stream()
.map(device -> recipient.requireServiceId().toProtocolAddress(device))
.map(device -> targetRecipient.requireServiceId().toProtocolAddress(device))
.collect(Collectors.toList());

ApplicationDependencies.getProtocolStore().aci().markSenderKeySharedWith(distributionId, addresses);
Expand All @@ -128,8 +152,8 @@ public static final class Factory implements Job.Factory<SenderKeyDistributionSe

@Override
public @NonNull SenderKeyDistributionSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new SenderKeyDistributionSendJob(RecipientId.from(data.getString(KEY_RECIPIENT_ID)),
GroupId.pushOrThrow(data.getStringAsBlob(KEY_GROUP_ID)).requireV2(),
return new SenderKeyDistributionSendJob(RecipientId.from(data.getString(KEY_TARGET_RECIPIENT_ID)),
RecipientId.from(data.getString(KEY_THREAD_RECIPIENT_ID)),
parameters);
}
}
Expand Down
Expand Up @@ -673,7 +673,7 @@ public StorySendOperation(@NonNull MessageId relatedMessageId,

@Override
public @NonNull ContentHint getContentHint() {
return ContentHint.RESENDABLE;
return ContentHint.IMPLICIT;
}

@Override
Expand Down

0 comments on commit c6be427

Please sign in to comment.