Support recovering forgotten/unknown group info from sender

Closes #5876
// FREEBIE
This commit is contained in:
Moxie Marlinspike
2016-11-20 14:50:41 -08:00
parent 10abd09239
commit cf01959e16
7 changed files with 233 additions and 13 deletions

View File

@@ -81,6 +81,10 @@ public class GroupDatabase extends Database {
return record;
}
public boolean isUnknownGroup(byte[] groupId) {
return getGroup(groupId) == null;
}
public Reader getGroupsFilteredByTitle(String constraint) {
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, TITLE + " LIKE ?",
new String[]{"%" + constraint + "%"},
@@ -129,6 +133,7 @@ public class GroupDatabase extends Database {
contentValues.put(ACTIVE, 1);
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
notifyConversationListListeners();
}
public void update(byte[] groupId, String title, SignalServiceAttachmentPointer avatar) {
@@ -147,6 +152,7 @@ public class GroupDatabase extends Database {
RecipientFactory.clearCache(context);
notifyDatabaseListeners();
notifyConversationListListeners();
}
public void updateTitle(byte[] groupId, String title) {

View File

@@ -15,11 +15,13 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.TextSecurePushTrustStore;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
@@ -49,7 +51,9 @@ import dagger.Provides;
MultiDeviceBlockedUpdateJob.class,
DeviceListFragment.class,
RefreshAttributesJob.class,
GcmRefreshJob.class})
GcmRefreshJob.class,
RequestGroupInfoJob.class,
PushGroupUpdateJob.class})
public class TextSecureCommunicationModule {
private final Context context;

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
@@ -29,6 +30,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import java.util.HashSet;
import java.util.LinkedList;
@@ -61,12 +63,14 @@ public class GroupMessageProcessor {
byte[] id = group.getGroupId();
GroupRecord record = database.getGroup(id);
if (record != null && group.getType() == SignalServiceGroup.Type.UPDATE) {
if (record != null && group.getType() == Type.UPDATE) {
return handleGroupUpdate(context, masterSecret, envelope, group, record, outgoing);
} else if (record == null && group.getType() == SignalServiceGroup.Type.UPDATE) {
} else if (record == null && group.getType() == Type.UPDATE) {
return handleGroupCreate(context, masterSecret, envelope, group, outgoing);
} else if (record != null && group.getType() == SignalServiceGroup.Type.QUIT) {
} else if (record != null && group.getType() == Type.QUIT) {
return handleGroupLeave(context, masterSecret, envelope, group, record, outgoing);
} else if (record != null && group.getType() == Type.REQUEST_INFO) {
return handleGroupInfoRequest(context, envelope, group, record);
} else {
Log.w(TAG, "Received unknown type, ignoring...");
return null;
@@ -144,6 +148,20 @@ public class GroupMessageProcessor {
return storeMessage(context, masterSecret, envelope, group, builder.build(), outgoing);
}
private static Long handleGroupInfoRequest(@NonNull Context context,
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceGroup group,
@NonNull GroupRecord record)
{
if (record.getMembers().contains(envelope.getSource())) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushGroupUpdateJob(context, envelope.getSource(), group.getGroupId()));
}
return null;
}
private static Long handleGroupLeave(@NonNull Context context,
@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
@@ -132,9 +133,10 @@ public class PushDecryptJob extends ContextJob {
private void handleMessage(MasterSecretUnion masterSecret, SignalServiceEnvelope envelope, Optional<Long> smsMessageId) {
try {
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore);
SignalServiceContent content = cipher.decrypt(envelope);
@@ -146,6 +148,10 @@ public class PushDecryptJob extends ContextJob {
else if (message.isExpirationUpdate()) handleExpirationUpdate(masterSecret, envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(message.getGroupInfo().get().getGroupId())) {
handleUnknownGroupMessage(envelope, message.getGroupInfo().get());
}
} else if (content.getSyncMessage().isPresent()) {
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
@@ -221,6 +227,14 @@ public class PushDecryptJob extends ContextJob {
}
}
private void handleUnknownGroupMessage(@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceGroup group)
{
ApplicationContext.getInstance(context)
.getJobManager()
.add(new RequestGroupInfoJob(context, envelope.getSource(), group.getGroupId()));
}
private void handleExpirationUpdate(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message,
@@ -254,6 +268,8 @@ public class PushDecryptJob extends ContextJob {
@NonNull Optional<Long> smsMessageId)
throws MmsException
{
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Long threadId;
if (message.getMessage().isGroupUpdate()) {
@@ -266,6 +282,10 @@ public class PushDecryptJob extends ContextJob {
threadId = handleSynchronizeSentTextMessage(masterSecret, message, smsMessageId);
}
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(message.getMessage().getGroupInfo().get().getGroupId())) {
handleUnknownGroupMessage(envelope, message.getMessage().getGroupInfo().get());
}
if (threadId != null) {
DatabaseFactory.getThreadDatabase(getContext()).setRead(threadId);
MessageNotifier.updateNotification(getContext(), masterSecret.getMasterSecret().orNull());

View File

@@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.inject.Inject;
public class PushGroupUpdateJob extends ContextJob implements InjectableType {
private static final String TAG = RequestGroupInfoJob.class.getSimpleName();
private static final long serialVersionUID = 0L;
@Inject transient TextSecureMessageSenderFactory messageSenderFactory;
private final String source;
private final byte[] groupId;
public PushGroupUpdateJob(Context context, String source, byte[] groupId) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new NetworkRequirement(context))
.withRetryCount(50)
.create());
this.source = source;
this.groupId = groupId;
}
@Override
public void onAdded() {}
@Override
public void onRun() throws IOException, UntrustedIdentityException {
SignalServiceMessageSender messageSender = messageSenderFactory.create();
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupRecord record = groupDatabase.getGroup(groupId);
if (record == null) {
Log.w(TAG, "No information for group record info request: " + new String(groupId));
return;
}
SignalServiceAttachment avatar = SignalServiceAttachmentStream.newStreamBuilder()
.withContentType("image/jpeg")
.withStream(new ByteArrayInputStream(record.getAvatar()))
.withLength(record.getAvatar().length)
.build();
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
.withAvatar(avatar)
.withId(groupId)
.withMembers(record.getMembers())
.withName(record.getTitle())
.build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
.asGroupMessage(groupContext)
.withTimestamp(System.currentTimeMillis())
.build();
messageSender.sendMessage(new SignalServiceAddress(source), message);
}
@Override
public boolean onShouldRetry(Exception e) {
Log.w(TAG, e);
return e instanceof PushNetworkException;
}
@Override
public void onCanceled() {
}
}

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule;
import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory;
import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import javax.inject.Inject;
public class RequestGroupInfoJob extends ContextJob implements InjectableType {
private static final String TAG = RequestGroupInfoJob.class.getSimpleName();
private static final long serialVersionUID = 0L;
@Inject transient TextSecureMessageSenderFactory messageSenderFactory;
private final String source;
private final byte[] groupId;
public RequestGroupInfoJob(@NonNull Context context, @NonNull String source, @NonNull byte[] groupId) {
super(context, JobParameters.newBuilder()
.withRequirement(new NetworkRequirement(context))
.withPersistence()
.withRetryCount(50)
.create());
this.source = source;
this.groupId = groupId;
}
@Override
public void onAdded() {}
@Override
public void onRun() throws IOException, UntrustedIdentityException {
SignalServiceMessageSender messageSender = messageSenderFactory.create();
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
.withId(groupId)
.build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group)
.withTimestamp(System.currentTimeMillis())
.build();
messageSender.sendMessage(new SignalServiceAddress(source), message);
}
@Override
public boolean onShouldRetry(Exception e) {
return e instanceof PushNetworkException;
}
@Override
public void onCanceled() {
}
}