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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
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());
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.