Support recovering forgotten/unknown group info from sender

Closes #5876
// FREEBIE
master
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

@ -73,7 +73,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile 'org.whispersystems:signal-service-android:2.4.0'
compile 'org.whispersystems:signal-service-android:2.4.1'
compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0'
compile 'com.google.zxing:core:3.2.1'
@ -131,7 +131,7 @@ dependencyVerification {
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
'org.whispersystems:signal-service-android:b018f599189b0321426a29c7f21e88e5c8c14debe613593ed728d1f522f85fe6',
'org.whispersystems:signal-service-android:b5449d6d74c16256591f675e599b1e588d13342ec8f52e2e598af0fa1e919259',
'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
@ -140,23 +140,24 @@ dependencyVerification {
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'org.whispersystems:signal-service-java:dda6c3b15872fee7a8980e0898a813aca6a603f8dc142d5354f30e2cc005ea17',
'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
'org.whispersystems:signal-service-java:4a55341f3a340a59c541d06fb80638a770d7ba9c2eac727757bea42c0a341ec5',
'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf',
'org.whispersystems:curve25519-android:bf6c34223d45d2f2813a8efcab9923caf99115115c760c9acea680bcb42d23c0',
'org.whispersystems:signal-protocol-java:a835cd0609cf116a74651bd0aa748db9392bba48c2d2af787757b8a1b50d131c',
'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'org.whispersystems:curve25519-android:bf6c34223d45d2f2813a8efcab9923caf99115115c760c9acea680bcb42d23c0',
'org.whispersystems:signal-protocol-java:a835cd0609cf116a74651bd0aa748db9392bba48c2d2af787757b8a1b50d131c',
'org.whispersystems:curve25519-java:00f1d4919f759055f41f7853a3d475dc7c8decf0dbf045ae93414f8f23b066cc',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'org.whispersystems:curve25519-java:00f1d4919f759055f41f7853a3d475dc7c8decf0dbf045ae93414f8f23b066cc',
'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423',
]
}
android {
compileSdkVersion 22
buildToolsVersion '23.0.2'

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() {
}
}