From 11d17f749645986ebefe2e6c3ef5d144f4c338f6 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 19 May 2020 18:48:26 -0300 Subject: [PATCH] GV2 storage service syncing. --- .../securesms/database/GroupDatabase.java | 2 +- .../securesms/database/RecipientDatabase.java | 146 ++++++++++++++---- .../securesms/jobs/StorageForcePushJob.java | 2 +- .../securesms/jobs/StorageSyncJob.java | 29 ++-- .../securesms/storage/StorageSyncHelper.java | 52 ++++++- .../securesms/storage/StorageSyncModels.java | 39 ++++- .../storage/StorageSyncHelperTest.java | 91 ++++++++--- .../api/storage/SignalGroupV2Record.java | 2 +- .../api/storage/SignalStorageRecord.java | 11 +- 9 files changed, 291 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 4a3e30320f..1ea674b243 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -64,7 +64,7 @@ public final class GroupDatabase extends Database { /* V2 Group columns */ /** 32 bytes serialized {@link GroupMasterKey} */ - private static final String V2_MASTER_KEY = "master_key"; + public static final String V2_MASTER_KEY = "master_key"; /** Increments with every change to the group */ private static final String V2_REVISION = "revision"; /** Serialized {@link DecryptedGroup} protobuf */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index a6e4e1fc85..6a058c135b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils; import net.sqlcipher.database.SQLiteDatabase; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.color.MaterialColor; @@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; +import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; @@ -44,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -125,27 +129,26 @@ public class RecipientDatabase extends Database { STORAGE_SERVICE_ID, DIRTY }; + private static final String[] ID_PROJECTION = new String[]{ID}; + private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; + public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME}; + static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) + .map(columnName -> TABLE_NAME + "." + columnName) + .toList().toArray(new String[0]); + private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat( new String[] { TABLE_NAME + "." + ID }, - RECIPIENT_PROJECTION, + TYPED_RECIPIENT_PROJECTION, new String[] { IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS, IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY }); - public static final String[] CREATE_INDEXS = new String[] { "CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");", "CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");", }; - private static final String[] ID_PROJECTION = new String[]{ID}; - private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; - public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME}; - static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) - .map(columnName -> TABLE_NAME + "." + columnName) - .toList(); - public enum VibrateState { DEFAULT(0), ENABLED(1), DISABLED(2); @@ -241,7 +244,7 @@ public class RecipientDatabase extends Database { } public enum GroupType { - NONE(0), MMS(1), SIGNAL_V1(2); + NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3); private final int id; @@ -366,7 +369,11 @@ public class RecipientDatabase extends Database { if (groupId.isMms()) { values.put(GROUP_TYPE, GroupType.MMS.getId()); } else { - values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); + if (groupId.isV2()) { + values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId()); + } else { + values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); + } values.put(DIRTY, DirtyState.INSERT.getId()); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); } @@ -423,29 +430,46 @@ public class RecipientDatabase extends Database { return DirtyState.CLEAN; } + public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) { + String query = TABLE_NAME + "." + ID + " = ?"; + String[] args = new String[]{id.serialize()}; + + List recipientSettingsForSync = getRecipientSettingsForSync(query, args); + + if (recipientSettingsForSync.isEmpty()) { + return null; + } + + if (recipientSettingsForSync.size() > 1) { + throw new AssertionError(); + } + + return recipientSettingsForSync.get(0); + } + public @NonNull List getPendingRecipientSyncUpdates() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() }; - return getRecipientSettings(query, args); + return getRecipientSettingsForSync(query, args); } public @NonNull List getPendingRecipientSyncInsertions() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() }; - return getRecipientSettings(query, args); + return getRecipientSettingsForSync(query, args); } public @NonNull List getPendingRecipientSyncDeletions() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() }; - return getRecipientSettings(query, args); + return getRecipientSettingsForSync(query, args); } public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) { - List result = getRecipientSettings(STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) }); + List result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) }); if (result.size() > 0) { return result.get(0); @@ -481,7 +505,9 @@ public class RecipientDatabase extends Database { public void applyStorageSyncUpdates(@NonNull Collection contactInserts, @NonNull Collection> contactUpdates, @NonNull Collection groupV1Inserts, - @NonNull Collection> groupV1Updates) + @NonNull Collection> groupV1Updates, + @NonNull Collection groupV2Inserts, + @NonNull Collection> groupV2Updates) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); @@ -586,6 +612,32 @@ public class RecipientDatabase extends Database { threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); recipient.live().refresh(); } + + for (SignalGroupV2Record insert : groupV2Inserts) { + db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert)); + + GroupId.V2 groupId = GroupId.v2(insert.getMasterKey()); + Recipient recipient = Recipient.externalGroup(context, groupId); + + ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); + + threadDatabase.setArchived(recipient.getId(), insert.isArchived()); + recipient.live().refresh(); + } + + for (RecordUpdate update : groupV2Updates) { + ContentValues values = getValuesForStorageGroupV2(update.getNew()); + int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())}); + + if (updateCount < 1) { + throw new AssertionError("Had an update, but it didn't match any rows!"); + } + + Recipient recipient = Recipient.externalGroup(context, GroupId.v2(update.getOld().getMasterKey())); + + threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); + recipient.live().refresh(); + } db.setTransactionSuccessful(); } finally { @@ -684,13 +736,29 @@ public class RecipientDatabase extends Database { values.put(DIRTY, DirtyState.CLEAN.getId()); return values; } + + private static @NonNull ContentValues getValuesForStorageGroupV2(@NonNull SignalGroupV2Record groupV2) { + ContentValues values = new ContentValues(); + values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKey()).toString()); + values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId()); + values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0"); + values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0"); + values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw())); + values.put(DIRTY, DirtyState.CLEAN.getId()); + return values; + } - private List getRecipientSettings(@Nullable String query, @Nullable String[] args) { + private List getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID; + String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID + + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID; List out = new ArrayList<>(); - try (Cursor cursor = db.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) { + String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION, + new String[]{ GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID, + GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }); + + try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) { while (cursor != null && cursor.moveToNext()) { out.add(getRecipientSettings(context, cursor)); } @@ -722,10 +790,11 @@ public class RecipientDatabase extends Database { GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE))); byte[] key = Base64.decodeOrThrow(encodedKey); - if (groupType == GroupType.NONE) { - out.put(id, StorageId.forContact(key)); - } else { - out.put(id, StorageId.forGroupV1(key)); + switch (groupType) { + case NONE : out.put(id, StorageId.forContact(key)); break; + case SIGNAL_V1 : out.put(id, StorageId.forGroupV1(key)); break; + case SIGNAL_V2 : out.put(id, StorageId.forGroupV2(key)); break; + default : throw new AssertionError(); } } } @@ -771,6 +840,19 @@ public class RecipientDatabase extends Database { String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS)); + int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY); + GroupMasterKey groupMasterKey = null; + try { + if (masterKeyIndex != -1) { + byte[] blob = cursor.getBlob(masterKeyIndex); + if (blob != null) { + groupMasterKey = new GroupMasterKey(blob); + } + } + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + MaterialColor color; byte[] profileKey = null; byte[] profileKeyCredential = null; @@ -805,7 +887,7 @@ public class RecipientDatabase extends Database { IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw); - return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, GroupType.fromId(groupType), blocked, muteUntil, + return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil, VibrateState.fromId(messageVibrateState), VibrateState.fromId(callVibrateState), Util.uri(messageRingtone), Util.uri(callRingtone), @@ -1745,6 +1827,7 @@ public class RecipientDatabase extends Database { private final String e164; private final String email; private final GroupId groupId; + private final GroupMasterKey groupMasterKey; private final GroupType groupType; private final boolean blocked; private final long muteUntil; @@ -1782,6 +1865,7 @@ public class RecipientDatabase extends Database { @Nullable String e164, @Nullable String email, @Nullable GroupId groupId, + @Nullable GroupMasterKey groupMasterKey, @NonNull GroupType groupType, boolean blocked, long muteUntil, @@ -1819,6 +1903,7 @@ public class RecipientDatabase extends Database { this.e164 = e164; this.email = email; this.groupId = groupId; + this.groupMasterKey = groupMasterKey; this.groupType = groupType; this.blocked = blocked; this.muteUntil = muteUntil; @@ -1875,6 +1960,13 @@ public class RecipientDatabase extends Database { return groupId; } + /** + * Only read populated for sync. + */ + public @Nullable GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + public @NonNull GroupType getGroupType() { return groupType; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index 6952c8c0bf..362ffa903c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -85,7 +85,7 @@ public class StorageForcePushJob extends BaseJob { Map newContactStorageIds = generateContactStorageIds(oldContactStorageIds); Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); List inserts = Stream.of(oldContactStorageIds.keySet()) - .map(recipientDatabase::getRecipientSettings) + .map(recipientDatabase::getRecipientSettingsForSync) .withoutNulls() .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients)) .toList(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index 1c7ebccc6b..fe6399c6a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -6,13 +6,6 @@ import androidx.annotation.NonNull; import com.annimon.stream.Stream; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult; -import org.thoughtcrime.securesms.storage.StorageSyncModels; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; @@ -23,22 +16,28 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; +import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult; +import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult; +import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult; +import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult; +import org.thoughtcrime.securesms.storage.StorageSyncModels; import org.thoughtcrime.securesms.storage.StorageSyncValidations; import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.storage.StorageId; -import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import java.io.IOException; @@ -183,7 +182,7 @@ public class StorageSyncJob extends BaseJob { Log.i(TAG, "[Remote Newer] After resolving the conflict, all changes are local. No remote writes needed."); } - recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates()); + recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates(), mergeResult.getLocalGroupV2Inserts(), mergeResult.getLocalGroupV2Updates()); storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes()); StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate()); needsMultiDeviceSync = true; @@ -277,7 +276,11 @@ public class StorageSyncJob extends BaseJob { case ManifestRecord.Identifier.Type.GROUPV2_VALUE: RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw()); if (settings != null) { - records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients)); + if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && settings.getGroupMasterKey() == null) { + Log.w(TAG, "Missing master key on gv2 recipient"); + } else { + records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients)); + } } else { Log.w(TAG, "Missing local recipient model! Type: " + id.getType()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java index 1ebe8075e2..028556f24e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.SetUtil; @@ -26,6 +25,7 @@ import org.whispersystems.libsignal.util.guava.Optional; 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.SignalRecord; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; @@ -46,8 +46,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; -import javax.crypto.KeyGenerator; - public final class StorageSyncHelper { private static final String TAG = Log.tag(StorageSyncHelper.class); @@ -116,6 +114,11 @@ public final class StorageSyncHelper { Map storageKeyUpdates = new HashMap<>(); for (RecipientSettings insert : inserts) { + if (insert.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && insert.getGroupMasterKey() == null) { + Log.w(TAG, "Missing master key on gv2 recipient"); + continue; + } + storageInserts.add(StorageSyncModels.localToRemoteRecord(insert, archivedRecipients)); switch (insert.getGroupType()) { @@ -125,6 +128,9 @@ public final class StorageSyncHelper { case SIGNAL_V1: completeIds.add(StorageId.forGroupV1(insert.getStorageId())); break; + case SIGNAL_V2: + completeIds.add(StorageId.forGroupV2(insert.getStorageId())); + break; default: throw new AssertionError("Unsupported type!"); } @@ -154,6 +160,10 @@ public final class StorageSyncHelper { oldId = StorageId.forGroupV1(update.getStorageId()); newId = StorageId.forGroupV1(generateKey()); break; + case SIGNAL_V2: + oldId = StorageId.forGroupV2(update.getStorageId()); + newId = StorageId.forGroupV2(generateKey()); + break; default: throw new AssertionError("Unsupported type!"); } @@ -226,10 +236,12 @@ public final class StorageSyncHelper { List remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); List localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); + + List remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList(); + List localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList(); - // TODO [storage] Handle groupV2 when appropriate - List remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList(); - List localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList(); + List remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList(); + List localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList(); List remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); List localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); @@ -242,11 +254,13 @@ public final class StorageSyncHelper { RecordMergeResult contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self())); RecordMergeResult groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1)); + RecordMergeResult groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2)); RecordMergeResult accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0)))); Set remoteInserts = new HashSet<>(); remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList()); remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList()); + remoteInserts.addAll(Stream.of(groupV2MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV2).toList()); remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList()); Set> remoteUpdates = new HashSet<>(); @@ -256,6 +270,9 @@ public final class StorageSyncHelper { remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates) .map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew()))) .toList()); + remoteUpdates.addAll(Stream.of(groupV2MergeResult.remoteUpdates) + .map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew()))) + .toList()); remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates) .map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew()))) .toList()); @@ -269,6 +286,8 @@ public final class StorageSyncHelper { contactMergeResult.localUpdates, groupV1MergeResult.localInserts, groupV1MergeResult.localUpdates, + groupV2MergeResult.localInserts, + groupV2MergeResult.localUpdates, new LinkedHashSet<>(remoteOnlyUnknowns), new LinkedHashSet<>(localOnlyUnknowns), accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()), @@ -449,6 +468,8 @@ public final class StorageSyncHelper { private final Set> localContactUpdates; private final Set localGroupV1Inserts; private final Set> localGroupV1Updates; + private final Set localGroupV2Inserts; + private final Set> localGroupV2Updates; private final Set localUnknownInserts; private final Set localUnknownDeletes; private final Optional> localAccountUpdate; @@ -461,6 +482,8 @@ public final class StorageSyncHelper { @NonNull Set> localContactUpdates, @NonNull Set localGroupV1Inserts, @NonNull Set> localGroupV1Updates, + @NonNull Set localGroupV2Inserts, + @NonNull Set> localGroupV2Updates, @NonNull Set localUnknownInserts, @NonNull Set localUnknownDeletes, @NonNull Optional> localAccountUpdate, @@ -472,6 +495,8 @@ public final class StorageSyncHelper { this.localContactUpdates = localContactUpdates; this.localGroupV1Inserts = localGroupV1Inserts; this.localGroupV1Updates = localGroupV1Updates; + this.localGroupV2Inserts = localGroupV2Inserts; + this.localGroupV2Updates = localGroupV2Updates; this.localUnknownInserts = localUnknownInserts; this.localUnknownDeletes = localUnknownDeletes; this.localAccountUpdate = localAccountUpdate; @@ -495,6 +520,14 @@ public final class StorageSyncHelper { public @NonNull Set> getLocalGroupV1Updates() { return localGroupV1Updates; } + + public @NonNull Set getLocalGroupV2Inserts() { + return localGroupV2Inserts; + } + + public @NonNull Set> getLocalGroupV2Updates() { + return localGroupV2Updates; + } public @NonNull Set getLocalUnknownInserts() { return localUnknownInserts; @@ -525,10 +558,12 @@ public final class StorageSyncHelper { records.addAll(localContactInserts); records.addAll(localGroupV1Inserts); + records.addAll(localGroupV2Inserts); records.addAll(remoteInserts); records.addAll(localUnknownInserts); records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList()); records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getNew).toList()); + records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getNew).toList()); records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList()); if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew()); @@ -541,6 +576,7 @@ public final class StorageSyncHelper { records.addAll(localUnknownDeletes); records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList()); records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getOld).toList()); + records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getOld).toList()); records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList()); records.addAll(remoteDeletes); if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld()); @@ -551,8 +587,8 @@ public final class StorageSyncHelper { @Override public @NonNull String toString() { return String.format(Locale.ENGLISH, - "localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d", - localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size()); + "localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localGroupV2Inserts: %d, localGroupV2Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d", + localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localGroupV2Inserts.size(), localGroupV2Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index 5faec13454..63800c9afe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -2,14 +2,16 @@ package org.thoughtcrime.securesms.storage; import androidx.annotation.NonNull; +import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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 org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; import java.util.Set; @@ -30,6 +32,7 @@ public final class StorageSyncModels { switch (settings.getGroupType()) { case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived)); case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId, archived)); + case SIGNAL_V2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, archived)); default: throw new AssertionError("Unsupported type!"); } } @@ -52,11 +55,41 @@ public final class StorageSyncModels { } private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set archived) { - if (recipient.getGroupId() == null) { + GroupId groupId = recipient.getGroupId(); + + if (groupId == null) { throw new AssertionError("Must have a groupId!"); } - return new SignalGroupV1Record.Builder(rawStorageId, recipient.getGroupId().getDecodedId()) + if (!groupId.isV1()) { + throw new AssertionError("Group is not V1"); + } + + return new SignalGroupV1Record.Builder(rawStorageId, groupId.getDecodedId()) + .setBlocked(recipient.isBlocked()) + .setProfileSharingEnabled(recipient.isProfileSharing()) + .setArchived(archived.contains(recipient.getId())) + .build(); + } + + private static @NonNull SignalGroupV2Record localToRemoteGroupV2(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set archived) { + GroupId groupId = recipient.getGroupId(); + + if (groupId == null) { + throw new AssertionError("Must have a groupId!"); + } + + if (!groupId.isV2()) { + throw new AssertionError("Group is not V2"); + } + + GroupMasterKey groupMasterKey = recipient.getGroupMasterKey(); + + if (groupMasterKey == null) { + throw new AssertionError("Group master key not on recipient record"); + } + + return new SignalGroupV2Record.Builder(rawStorageId, groupMasterKey) .setBlocked(recipient.isBlocked()) .setProfileSharingEnabled(recipient.isProfileSharing()) .setArchived(archived.contains(recipient.getId())) diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java index 3971f13b59..2cd50b9909 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.storage; import androidx.annotation.NonNull; -import com.airbnb.lottie.L; import com.annimon.stream.Stream; import org.junit.Before; @@ -28,6 +27,7 @@ import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -144,7 +144,7 @@ public final class StorageSyncHelperTest { } @Test - public void resolveConflict_group_sameAsRemote() { + public void resolveConflict_group_v1_sameAsRemote() { SignalGroupV1Record remote1 = groupV1(1, 1, true, false); SignalGroupV1Record local1 = groupV1(2, 1, true, false); @@ -158,6 +158,22 @@ public final class StorageSyncHelperTest { assertTrue(result.getRemoteUpdates().isEmpty()); assertTrue(result.getRemoteDeletes().isEmpty()); } + + @Test + public void resolveConflict_group_v2_sameAsRemote() { + SignalGroupV2Record remote1 = groupV2(1, 2, true, false); + SignalGroupV2Record local1 = groupV2(2, 2, true, false); + + MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1)); + + SignalGroupV2Record expectedMerge = groupV2(1, 2, true, false); + + assertTrue(result.getLocalContactInserts().isEmpty()); + assertEquals(setOf(update(local1, expectedMerge)), result.getLocalGroupV2Updates()); + assertTrue(result.getRemoteInserts().isEmpty()); + assertTrue(result.getRemoteUpdates().isEmpty()); + assertTrue(result.getRemoteDeletes().isEmpty()); + } @Test public void resolveConflict_contact_sameAsLocal() { @@ -176,7 +192,7 @@ public final class StorageSyncHelperTest { } @Test - public void resolveConflict_group_sameAsLocal() { + public void resolveConflict_group_v1_sameAsLocal() { SignalGroupV1Record remote1 = groupV1(1, 1, true, false); SignalGroupV1Record local1 = groupV1(2, 1, true, true); @@ -190,23 +206,37 @@ public final class StorageSyncHelperTest { assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates()); assertTrue(result.getRemoteDeletes().isEmpty()); } + + @Test + public void resolveConflict_group_v2_sameAsLocal() { + SignalGroupV2Record remote1 = groupV2(1, 2, true, false); + SignalGroupV2Record local1 = groupV2(2, 2, true, true); + + MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1)); + + SignalGroupV2Record expectedMerge = groupV2(2, 2, true, true); + + assertTrue(result.getLocalContactInserts().isEmpty()); + assertTrue(result.getLocalGroupV2Updates().isEmpty()); + assertTrue(result.getRemoteInserts().isEmpty()); + assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates()); + assertTrue(result.getRemoteDeletes().isEmpty()); + } @Test public void resolveConflict_unknowns() { SignalStorageRecord account = SignalStorageRecord.forAccount(account(99)); SignalStorageRecord remote1 = unknown(3); SignalStorageRecord remote2 = unknown(4); - SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true)); SignalStorageRecord local1 = unknown(1); SignalStorageRecord local2 = unknown(2); - SignalStorageRecord local3 = SignalStorageRecord.forGroupV2(groupV2(101, 201, true, true)); - MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3, account), setOf(local1, local2, local3, account)); + MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, account), setOf(local1, local2, account)); assertTrue(result.getLocalContactInserts().isEmpty()); assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts()); - assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes()); + assertEquals(setOf(remote1, remote2), result.getLocalUnknownInserts()); + assertEquals(setOf(local1, local2), result.getLocalUnknownDeletes()); assertTrue(result.getRemoteDeletes().isEmpty()); } @@ -224,29 +254,34 @@ public final class StorageSyncHelperTest { SignalGroupV1Record remote4 = groupV1(7, 1, true, false); SignalGroupV1Record local4 = groupV1(8, 1, false, true); - SignalAccountRecord remote5 = account(9); - SignalAccountRecord local5 = account(10); + SignalGroupV2Record remote5 = groupV2(9, 2, true, false); + SignalGroupV2Record local5 = groupV2(10, 2, false, true); - SignalStorageRecord unknownRemote = unknown(11); - SignalStorageRecord unknownLocal = unknown(12); + SignalAccountRecord remote6 = account(11); + SignalAccountRecord local6 = account(12); - StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222)); + SignalStorageRecord unknownRemote = unknown(13); + SignalStorageRecord unknownLocal = unknown(14); - Set remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote); - Set localOnly = recordSetOf(local1, local2, local3, local4, local5, unknownLocal); + StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222, 333)); + + Set remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, remote6, unknownRemote); + Set localOnly = recordSetOf(local1, local2, local3, local4, local5, local6, unknownLocal); MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly); SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a"); SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b"); SignalGroupV1Record merge4 = groupV1(222, 1, true, true); + SignalGroupV2Record merge5 = groupV2(333, 2, true, true); assertEquals(setOf(remote3), result.getLocalContactInserts()); assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates()); assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates()); + assertEquals(setOf(update(local5, merge5)), result.getLocalGroupV2Updates()); assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts()); - assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates()); - assertEquals(Optional.of(update(local5, remote5)), result.getLocalAccountUpdate()); + assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4), recordUpdate(remote5, merge5)), result.getRemoteUpdates()); + assertEquals(Optional.of(update(local6, remote6)), result.getLocalAccountUpdate()); assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts()); assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes()); assertTrue(result.getRemoteDeletes().isEmpty()); @@ -254,7 +289,7 @@ public final class StorageSyncHelperTest { @Test public void createWriteOperation_generic() { - List localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100)); + List localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100), groupV2Key(200)); SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a"); SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b"); SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z"); @@ -264,6 +299,9 @@ public final class StorageSyncHelperTest { SignalGroupV1Record insert3 = groupV1(9, 1, true, true); SignalGroupV1Record old3 = groupV1(100, 1, true, true); SignalGroupV1Record new3 = groupV1(10, 1, false, true); + SignalGroupV2Record insert4 = groupV2(19, 2, true, true); + SignalGroupV2Record old4 = groupV2(200, 2, true, true); + SignalGroupV2Record new4 = groupV2(20, 2, false, true); SignalStorageRecord unknownInsert = unknown(11); SignalStorageRecord unknownDelete = unknown(12); @@ -273,18 +311,19 @@ public final class StorageSyncHelperTest { setOf(update(old2, new2)), setOf(insert3), setOf(update(old3, new3)), + setOf(insert4), + setOf(update(old4, new4)), setOf(unknownInsert), setOf(unknownDelete), Optional.absent(), - recordSetOf(insert1, insert3), - setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)), + recordSetOf(insert1, insert3, insert4), + setOf(recordUpdate(old1, new1), recordUpdate(old3, new3), recordUpdate(old4, new4)), setOf())); assertEquals(2, result.getManifest().getVersion()); - assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), unknownKey(11)), result.getManifest().getStorageIds()); - assertTrue(recordSetOf(insert1, new1, insert3, new3).containsAll(result.getInserts())); - assertEquals(4, result.getInserts().size()); - assertByteListEquals(byteListOf(1, 100), result.getDeletes()); + assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), groupV2Key(19), groupV2Key(20), unknownKey(11)), result.getManifest().getStorageIds()); + assertEquals(recordSetOf(insert1, new1, insert3, new3, insert4, new4), new HashSet<>(result.getInserts())); + assertByteListEquals(byteListOf(1, 100, 200), result.getDeletes()); } @Test @@ -415,6 +454,10 @@ public final class StorageSyncHelperTest { return StorageId.forGroupV1(byteArray(val)); } + private static StorageId groupV2Key(int val) { + return StorageId.forGroupV2(byteArray(val)); + } + private static StorageId unknownKey(int val) { return StorageId.forType(byteArray(val), UNKNOWN_TYPE); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java index 3ad577d84f..cd5a5d4ac0 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java @@ -69,7 +69,7 @@ public final class SignalGroupV2Record implements SignalRecord { private final GroupV2Record.Builder builder; public Builder(byte[] rawId, GroupMasterKey masterKey) { - this.id = StorageId.forGroupV1(rawId); + this.id = StorageId.forGroupV2(rawId); this.builder = GroupV2Record.newBuilder(); builder.setMasterKey(ByteString.copyFrom(masterKey.serialize())); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java index d80a19eff7..4e029f3fc3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java @@ -7,7 +7,7 @@ import java.util.Objects; public class SignalStorageRecord implements SignalRecord { - private final StorageId id; + private final StorageId id; private final Optional contact; private final Optional groupV1; private final Optional groupV2; @@ -88,7 +88,7 @@ public class SignalStorageRecord implements SignalRecord { } public boolean isUnknown() { - return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent(); + return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent(); } @Override @@ -97,12 +97,13 @@ public class SignalStorageRecord implements SignalRecord { if (o == null || getClass() != o.getClass()) return false; SignalStorageRecord that = (SignalStorageRecord) o; return Objects.equals(id, that.id) && - Objects.equals(contact, that.contact) && - Objects.equals(groupV1, that.groupV1); + Objects.equals(contact, that.contact) && + Objects.equals(groupV1, that.groupV1) && + Objects.equals(groupV2, that.groupV2); } @Override public int hashCode() { - return Objects.hash(id, contact, groupV1); + return Objects.hash(id, contact, groupV1, groupV2); } }