Add the ability to migrate GV1 groups to GV2.

Co-authored-by: Alan Evans <alan@signal.org>
This commit is contained in:
Greyson Parrelli
2020-10-15 15:49:09 -04:00
committed by Alan Evans
parent 2d1bf33902
commit 6bb9d27d4e
34 changed files with 818 additions and 132 deletions

View File

@@ -468,6 +468,48 @@ public final class GroupDatabase extends Database {
notifyConversationListListeners();
}
/**
* Migrates a V1 group to a V2 group.
*/
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
GroupId.V2 groupIdV2 = groupIdV1.deriveV2MigrationGroupId();
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
db.beginTransaction();
try {
GroupRecord record = getGroup(groupIdV1).get();
ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, groupIdV2.toString());
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
contentValues.putNull(EXPECTED_V2_ID);
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
newMembers.addAll(Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList());
if (record.getMembers().size() > newMembers.size() || !newMembers.containsAll(record.getMembers())) {
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
}
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
if (updated != 1) {
throw new AssertionError();
}
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
update(groupMasterKey, decryptedGroup);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return groupIdV2;
}
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
update(GroupId.v2(groupMasterKey), decryptedGroup);
}

View File

@@ -137,6 +137,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
public abstract void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients);
public abstract boolean deleteMessage(long messageId);
abstract void deleteThread(long threadId);

View File

@@ -414,6 +414,11 @@ public class MmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException();
}
@Override
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
throw new UnsupportedOperationException();
}
@Override
public void endTransaction(SQLiteDatabase database) {
database.endTransaction();
@@ -588,7 +593,7 @@ public class MmsDatabase extends MessageDatabase {
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
if (retrieved.getGroupId() != null) {
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(retrieved.getGroupId());
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId());
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
} else {

View File

@@ -40,6 +40,7 @@ public interface MmsSmsColumns {
protected static final long INVALID_MESSAGE_TYPE = 6;
protected static final long PROFILE_CHANGE_TYPE = 7;
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
protected static final long GV1_MIGRATION_TYPE = 9;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
@@ -377,8 +378,8 @@ public class RecipientDatabase extends Database {
return getByColumn(EMAIL, email);
}
public @NonNull Optional<RecipientId> getByGroupId(@NonNull String groupId) {
return getByColumn(GROUP_ID, groupId);
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
return getByColumn(GROUP_ID, groupId.toString());
}
@@ -554,7 +555,7 @@ public class RecipientDatabase extends Database {
}
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
Optional<RecipientId> existing = getByGroupId(groupId);
if (existing.isPresent()) {
return existing.get();
@@ -604,6 +605,44 @@ public class RecipientDatabase extends Database {
}
}
/**
* See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}.
*/
public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
if (existing.isPresent()) {
return existing.get();
}
if (groupId.isV1()) {
Optional<RecipientId> v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId());
if (v2.isPresent()) {
return v2.get();
}
}
if (groupId.isV2()) {
Optional<GroupDatabase.GroupRecord> v1 = DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2());
if (v1.isPresent()) {
return v1.get().getRecipientId();
}
}
RecipientId id = getOrInsertFromGroupId(groupId);
db.setTransactionSuccessful();
return id;
} finally {
db.endTransaction();
}
}
public Cursor getBlocked() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
@@ -877,7 +916,7 @@ public class RecipientDatabase extends Database {
for (SignalGroupV1Record insert : groupV1Inserts) {
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
needsRefresh.add(recipient.getId());
@@ -891,7 +930,7 @@ public class RecipientDatabase extends Database {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
@@ -902,7 +941,7 @@ public class RecipientDatabase extends Database {
GroupId.V2 groupId = GroupId.v2(masterKey);
ContentValues values = getValuesForStorageGroupV2(insert);
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
Recipient recipient = Recipient.externalGroup(context, groupId);
Recipient recipient = Recipient.externalGroupExact(context, groupId);
if (id < 0) {
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
@@ -934,7 +973,7 @@ public class RecipientDatabase extends Database {
}
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
@@ -1155,7 +1194,7 @@ public class RecipientDatabase extends Database {
}
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
Recipient recipient = Recipient.externalGroup(context, id);
Recipient recipient = Recipient.externalGroupExact(context, id);
RecipientId recipientId = recipient.getId();
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
@@ -2300,6 +2339,24 @@ public class RecipientDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
}
/**
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
* migration.
*/
void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) {
ContentValues values = new ContentValues();
values.put(GROUP_ID, v2Id.toString());
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values);
if (update(query, values)) {
RecipientId id = getByGroupId(v2Id).get();
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
}
}
/**
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.

View File

@@ -739,6 +739,24 @@ public class SmsDatabase extends MessageDatabase {
}
}
@Override
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, System.currentTimeMillis());
values.put(DATE_SENT, System.currentTimeMillis());
values.put(READ, 1);
values.put(TYPE, Types.GV1_MIGRATION_TYPE);
values.put(THREAD_ID, threadId);
values.put(BODY, RecipientId.toSerializedList(pendingRecipients));
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
}
@Override
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
if (message.isJoined()) {
@@ -775,7 +793,7 @@ public class SmsDatabase extends MessageDatabase {
if (message.getGroupId() == null) {
groupRecipient = null;
} else {
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(message.getGroupId());
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(message.getGroupId());
groupRecipient = Recipient.resolved(id);
}

View File

@@ -62,7 +62,6 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import java.io.Closeable;
import java.io.IOException;
@@ -577,6 +576,28 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null);
}
public @NonNull List<ThreadRecord> getRecentV1Groups(int limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = MESSAGE_COUNT + " != 0 AND " +
"(" +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1 AND " +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY + " IS NULL AND " +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.MMS + " = 0" +
")";
String query = createQuery(where, 0, limit, true);
List<ThreadRecord> threadRecords = new ArrayList<>();
try (Reader reader = readerFor(db.rawQuery(query, null))) {
ThreadRecord record;
while ((record = reader.getNext()) != null) {
threadRecords.add(record);
}
}
return threadRecords;
}
public Cursor getConversationList() {
return getConversationList("0");
}
@@ -697,7 +718,7 @@ public class ThreadDatabase extends Database {
final String query;
if (pinned) {
query = createQuery(where, PINNED + " ASC", offset, limit, false);
query = createQuery(where, PINNED + " ASC", offset, limit);
} else {
query = createQuery(where, offset, limit, false);
}
@@ -1076,14 +1097,14 @@ public class ThreadDatabase extends Database {
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
} else if (pinned.getGroupV1Id().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroup(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
} catch (BadGroupIdException e) {
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
pinnedRecipient = null;
}
} else if (pinned.getGroupV2MasterKey().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroup(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
} catch (InvalidInputException e) {
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
pinnedRecipient = null;
@@ -1321,10 +1342,10 @@ public class ThreadDatabase extends Database {
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
return createQuery(where, orderBy, offset, limit, preferPinned);
return createQuery(where, orderBy, offset, limit);
}
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit, boolean preferPinned) {
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit) {
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
String query =