GV2 storage service syncing.

This commit is contained in:
Alan Evans 2020-05-19 18:48:26 -03:00 committed by Greyson Parrelli
parent 36df3f234f
commit 11d17f7496
9 changed files with 291 additions and 83 deletions

View File

@ -64,7 +64,7 @@ public final class GroupDatabase extends Database {
/* V2 Group columns */ /* V2 Group columns */
/** 32 bytes serialized {@link GroupMasterKey} */ /** 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 */ /** Increments with every change to the group */
private static final String V2_REVISION = "revision"; private static final String V2_REVISION = "revision";
/** Serialized {@link DecryptedGroup} protobuf */ /** Serialized {@link DecryptedGroup} protobuf */

View File

@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteDatabase; 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.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.color.MaterialColor; 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.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName; 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.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; 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.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
@ -125,27 +129,26 @@ public class RecipientDatabase extends Database {
STORAGE_SERVICE_ID, DIRTY 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( private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
new String[] { TABLE_NAME + "." + ID }, new String[] { TABLE_NAME + "." + ID },
RECIPIENT_PROJECTION, TYPED_RECIPIENT_PROJECTION,
new String[] { new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS, IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
}); });
public static final String[] CREATE_INDEXS = new String[] { 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_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");", "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<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList();
public enum VibrateState { public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2); DEFAULT(0), ENABLED(1), DISABLED(2);
@ -241,7 +244,7 @@ public class RecipientDatabase extends Database {
} }
public enum GroupType { public enum GroupType {
NONE(0), MMS(1), SIGNAL_V1(2); NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3);
private final int id; private final int id;
@ -366,7 +369,11 @@ public class RecipientDatabase extends Database {
if (groupId.isMms()) { if (groupId.isMms()) {
values.put(GROUP_TYPE, GroupType.MMS.getId()); values.put(GROUP_TYPE, GroupType.MMS.getId());
} else { } 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(DIRTY, DirtyState.INSERT.getId());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
} }
@ -423,29 +430,46 @@ public class RecipientDatabase extends Database {
return DirtyState.CLEAN; return DirtyState.CLEAN;
} }
public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) {
String query = TABLE_NAME + "." + ID + " = ?";
String[] args = new String[]{id.serialize()};
List<RecipientSettings> recipientSettingsForSync = getRecipientSettingsForSync(query, args);
if (recipientSettingsForSync.isEmpty()) {
return null;
}
if (recipientSettingsForSync.size() > 1) {
throw new AssertionError();
}
return recipientSettingsForSync.get(0);
}
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() { public @NonNull List<RecipientSettings> 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() }; String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
return getRecipientSettings(query, args); return getRecipientSettingsForSync(query, args);
} }
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() { public @NonNull List<RecipientSettings> 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() }; String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
return getRecipientSettings(query, args); return getRecipientSettingsForSync(query, args);
} }
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() { public @NonNull List<RecipientSettings> 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() }; 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) { public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) {
List<RecipientSettings> result = getRecipientSettings(STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) }); List<RecipientSettings> result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
if (result.size() > 0) { if (result.size() > 0) {
return result.get(0); return result.get(0);
@ -481,7 +505,9 @@ public class RecipientDatabase extends Database {
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts, public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates, @NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
@NonNull Collection<SignalGroupV1Record> groupV1Inserts, @NonNull Collection<SignalGroupV1Record> groupV1Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates) @NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates,
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV2Record>> groupV2Updates)
{ {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
@ -586,6 +612,32 @@ public class RecipientDatabase extends Database {
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
recipient.live().refresh(); 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<SignalGroupV2Record> 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(); db.setTransactionSuccessful();
} finally { } finally {
@ -684,13 +736,29 @@ public class RecipientDatabase extends Database {
values.put(DIRTY, DirtyState.CLEAN.getId()); values.put(DIRTY, DirtyState.CLEAN.getId());
return values; 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<RecipientSettings> getRecipientSettings(@Nullable String query, @Nullable String[] args) { private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); 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<RecipientSettings> out = new ArrayList<>(); List<RecipientSettings> 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()) { while (cursor != null && cursor.moveToNext()) {
out.add(getRecipientSettings(context, cursor)); out.add(getRecipientSettings(context, cursor));
} }
@ -722,10 +790,11 @@ public class RecipientDatabase extends Database {
GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE))); GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE)));
byte[] key = Base64.decodeOrThrow(encodedKey); byte[] key = Base64.decodeOrThrow(encodedKey);
if (groupType == GroupType.NONE) { switch (groupType) {
out.put(id, StorageId.forContact(key)); case NONE : out.put(id, StorageId.forContact(key)); break;
} else { case SIGNAL_V1 : out.put(id, StorageId.forGroupV1(key)); break;
out.put(id, StorageId.forGroupV1(key)); 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)); String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS)); 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; MaterialColor color;
byte[] profileKey = null; byte[] profileKey = null;
byte[] profileKeyCredential = null; byte[] profileKeyCredential = null;
@ -805,7 +887,7 @@ public class RecipientDatabase extends Database {
IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw); 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(messageVibrateState),
VibrateState.fromId(callVibrateState), VibrateState.fromId(callVibrateState),
Util.uri(messageRingtone), Util.uri(callRingtone), Util.uri(messageRingtone), Util.uri(callRingtone),
@ -1745,6 +1827,7 @@ public class RecipientDatabase extends Database {
private final String e164; private final String e164;
private final String email; private final String email;
private final GroupId groupId; private final GroupId groupId;
private final GroupMasterKey groupMasterKey;
private final GroupType groupType; private final GroupType groupType;
private final boolean blocked; private final boolean blocked;
private final long muteUntil; private final long muteUntil;
@ -1782,6 +1865,7 @@ public class RecipientDatabase extends Database {
@Nullable String e164, @Nullable String e164,
@Nullable String email, @Nullable String email,
@Nullable GroupId groupId, @Nullable GroupId groupId,
@Nullable GroupMasterKey groupMasterKey,
@NonNull GroupType groupType, @NonNull GroupType groupType,
boolean blocked, boolean blocked,
long muteUntil, long muteUntil,
@ -1819,6 +1903,7 @@ public class RecipientDatabase extends Database {
this.e164 = e164; this.e164 = e164;
this.email = email; this.email = email;
this.groupId = groupId; this.groupId = groupId;
this.groupMasterKey = groupMasterKey;
this.groupType = groupType; this.groupType = groupType;
this.blocked = blocked; this.blocked = blocked;
this.muteUntil = muteUntil; this.muteUntil = muteUntil;
@ -1875,6 +1960,13 @@ public class RecipientDatabase extends Database {
return groupId; return groupId;
} }
/**
* Only read populated for sync.
*/
public @Nullable GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public @NonNull GroupType getGroupType() { public @NonNull GroupType getGroupType() {
return groupType; return groupType;
} }

View File

@ -85,7 +85,7 @@ public class StorageForcePushJob extends BaseJob {
Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds); Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> inserts = Stream.of(oldContactStorageIds.keySet()) List<SignalStorageRecord> inserts = Stream.of(oldContactStorageIds.keySet())
.map(recipientDatabase::getRecipientSettings) .map(recipientDatabase::getRecipientSettingsForSync)
.withoutNulls() .withoutNulls()
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients)) .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients))
.toList(); .toList();

View File

@ -6,13 +6,6 @@ import androidx.annotation.NonNull;
import com.annimon.stream.Stream; 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.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; 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.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; 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.storage.StorageSyncValidations;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; 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 org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.io.IOException; 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."); 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()); storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes());
StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate()); StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate());
needsMultiDeviceSync = true; needsMultiDeviceSync = true;
@ -277,7 +276,11 @@ public class StorageSyncJob extends BaseJob {
case ManifestRecord.Identifier.Type.GROUPV2_VALUE: case ManifestRecord.Identifier.Type.GROUPV2_VALUE:
RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw()); RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw());
if (settings != null) { 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 { } else {
Log.w(TAG, "Missing local recipient model! Type: " + id.getType()); Log.w(TAG, "Missing local recipient model! Type: " + id.getType());
} }

View File

@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SetUtil; 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.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; 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.SignalRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
@ -46,8 +46,6 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.crypto.KeyGenerator;
public final class StorageSyncHelper { public final class StorageSyncHelper {
private static final String TAG = Log.tag(StorageSyncHelper.class); private static final String TAG = Log.tag(StorageSyncHelper.class);
@ -116,6 +114,11 @@ public final class StorageSyncHelper {
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>(); Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
for (RecipientSettings insert : inserts) { 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)); storageInserts.add(StorageSyncModels.localToRemoteRecord(insert, archivedRecipients));
switch (insert.getGroupType()) { switch (insert.getGroupType()) {
@ -125,6 +128,9 @@ public final class StorageSyncHelper {
case SIGNAL_V1: case SIGNAL_V1:
completeIds.add(StorageId.forGroupV1(insert.getStorageId())); completeIds.add(StorageId.forGroupV1(insert.getStorageId()));
break; break;
case SIGNAL_V2:
completeIds.add(StorageId.forGroupV2(insert.getStorageId()));
break;
default: default:
throw new AssertionError("Unsupported type!"); throw new AssertionError("Unsupported type!");
} }
@ -154,6 +160,10 @@ public final class StorageSyncHelper {
oldId = StorageId.forGroupV1(update.getStorageId()); oldId = StorageId.forGroupV1(update.getStorageId());
newId = StorageId.forGroupV1(generateKey()); newId = StorageId.forGroupV1(generateKey());
break; break;
case SIGNAL_V2:
oldId = StorageId.forGroupV2(update.getStorageId());
newId = StorageId.forGroupV2(generateKey());
break;
default: default:
throw new AssertionError("Unsupported type!"); throw new AssertionError("Unsupported type!");
} }
@ -226,10 +236,12 @@ public final class StorageSyncHelper {
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
List<SignalGroupV2Record> remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
List<SignalGroupV2Record> localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
// TODO [storage] Handle groupV2 when appropriate List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList(); List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
@ -242,11 +254,13 @@ public final class StorageSyncHelper {
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self())); RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1)); RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
RecordMergeResult<SignalGroupV2Record> groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2));
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0)))); RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
Set<SignalStorageRecord> remoteInserts = new HashSet<>(); Set<SignalStorageRecord> remoteInserts = new HashSet<>();
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList()); remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).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()); remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
Set<RecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>(); Set<RecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
@ -256,6 +270,9 @@ public final class StorageSyncHelper {
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates) remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew()))) .map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
.toList()); .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) remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew()))) .map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
.toList()); .toList());
@ -269,6 +286,8 @@ public final class StorageSyncHelper {
contactMergeResult.localUpdates, contactMergeResult.localUpdates,
groupV1MergeResult.localInserts, groupV1MergeResult.localInserts,
groupV1MergeResult.localUpdates, groupV1MergeResult.localUpdates,
groupV2MergeResult.localInserts,
groupV2MergeResult.localUpdates,
new LinkedHashSet<>(remoteOnlyUnknowns), new LinkedHashSet<>(remoteOnlyUnknowns),
new LinkedHashSet<>(localOnlyUnknowns), new LinkedHashSet<>(localOnlyUnknowns),
accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()), accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()),
@ -449,6 +468,8 @@ public final class StorageSyncHelper {
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates; private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts; private final Set<SignalGroupV1Record> localGroupV1Inserts;
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates; private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
private final Set<SignalGroupV2Record> localGroupV2Inserts;
private final Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates;
private final Set<SignalStorageRecord> localUnknownInserts; private final Set<SignalStorageRecord> localUnknownInserts;
private final Set<SignalStorageRecord> localUnknownDeletes; private final Set<SignalStorageRecord> localUnknownDeletes;
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate; private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
@ -461,6 +482,8 @@ public final class StorageSyncHelper {
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates, @NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts, @NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates, @NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
@NonNull Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts, @NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes, @NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate, @NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate,
@ -472,6 +495,8 @@ public final class StorageSyncHelper {
this.localContactUpdates = localContactUpdates; this.localContactUpdates = localContactUpdates;
this.localGroupV1Inserts = localGroupV1Inserts; this.localGroupV1Inserts = localGroupV1Inserts;
this.localGroupV1Updates = localGroupV1Updates; this.localGroupV1Updates = localGroupV1Updates;
this.localGroupV2Inserts = localGroupV2Inserts;
this.localGroupV2Updates = localGroupV2Updates;
this.localUnknownInserts = localUnknownInserts; this.localUnknownInserts = localUnknownInserts;
this.localUnknownDeletes = localUnknownDeletes; this.localUnknownDeletes = localUnknownDeletes;
this.localAccountUpdate = localAccountUpdate; this.localAccountUpdate = localAccountUpdate;
@ -495,6 +520,14 @@ public final class StorageSyncHelper {
public @NonNull Set<RecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() { public @NonNull Set<RecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
return localGroupV1Updates; return localGroupV1Updates;
} }
public @NonNull Set<SignalGroupV2Record> getLocalGroupV2Inserts() {
return localGroupV2Inserts;
}
public @NonNull Set<RecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
return localGroupV2Updates;
}
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() { public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
return localUnknownInserts; return localUnknownInserts;
@ -525,10 +558,12 @@ public final class StorageSyncHelper {
records.addAll(localContactInserts); records.addAll(localContactInserts);
records.addAll(localGroupV1Inserts); records.addAll(localGroupV1Inserts);
records.addAll(localGroupV2Inserts);
records.addAll(remoteInserts); records.addAll(remoteInserts);
records.addAll(localUnknownInserts); records.addAll(localUnknownInserts);
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList()); records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList());
records.addAll(Stream.of(localGroupV1Updates).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()); records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList());
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew()); if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew());
@ -541,6 +576,7 @@ public final class StorageSyncHelper {
records.addAll(localUnknownDeletes); records.addAll(localUnknownDeletes);
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList()); records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList());
records.addAll(Stream.of(localGroupV1Updates).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(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList());
records.addAll(remoteDeletes); records.addAll(remoteDeletes);
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld()); if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld());
@ -551,8 +587,8 @@ public final class StorageSyncHelper {
@Override @Override
public @NonNull String toString() { public @NonNull String toString() {
return String.format(Locale.ENGLISH, return String.format(Locale.ENGLISH,
"localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d", "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(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size()); localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localGroupV2Inserts.size(), localGroupV2Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size());
} }
} }

View File

@ -2,14 +2,16 @@ package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; 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.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.util.Set; import java.util.Set;
@ -30,6 +32,7 @@ public final class StorageSyncModels {
switch (settings.getGroupType()) { switch (settings.getGroupType()) {
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived)); case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived));
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(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!"); 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<RecipientId> archived) { private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
if (recipient.getGroupId() == null) { GroupId groupId = recipient.getGroupId();
if (groupId == null) {
throw new AssertionError("Must have a groupId!"); 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<RecipientId> 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()) .setBlocked(recipient.isBlocked())
.setProfileSharingEnabled(recipient.isProfileSharing()) .setProfileSharingEnabled(recipient.isProfileSharing())
.setArchived(archived.contains(recipient.getId())) .setArchived(archived.contains(recipient.getId()))

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.airbnb.lottie.L;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.junit.Before; import org.junit.Before;
@ -28,6 +27,7 @@ import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -144,7 +144,7 @@ public final class StorageSyncHelperTest {
} }
@Test @Test
public void resolveConflict_group_sameAsRemote() { public void resolveConflict_group_v1_sameAsRemote() {
SignalGroupV1Record remote1 = groupV1(1, 1, true, false); SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
SignalGroupV1Record local1 = groupV1(2, 1, true, false); SignalGroupV1Record local1 = groupV1(2, 1, true, false);
@ -158,6 +158,22 @@ public final class StorageSyncHelperTest {
assertTrue(result.getRemoteUpdates().isEmpty()); assertTrue(result.getRemoteUpdates().isEmpty());
assertTrue(result.getRemoteDeletes().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 @Test
public void resolveConflict_contact_sameAsLocal() { public void resolveConflict_contact_sameAsLocal() {
@ -176,7 +192,7 @@ public final class StorageSyncHelperTest {
} }
@Test @Test
public void resolveConflict_group_sameAsLocal() { public void resolveConflict_group_v1_sameAsLocal() {
SignalGroupV1Record remote1 = groupV1(1, 1, true, false); SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
SignalGroupV1Record local1 = groupV1(2, 1, true, true); SignalGroupV1Record local1 = groupV1(2, 1, true, true);
@ -190,23 +206,37 @@ public final class StorageSyncHelperTest {
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates()); assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
assertTrue(result.getRemoteDeletes().isEmpty()); 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 @Test
public void resolveConflict_unknowns() { public void resolveConflict_unknowns() {
SignalStorageRecord account = SignalStorageRecord.forAccount(account(99)); SignalStorageRecord account = SignalStorageRecord.forAccount(account(99));
SignalStorageRecord remote1 = unknown(3); SignalStorageRecord remote1 = unknown(3);
SignalStorageRecord remote2 = unknown(4); SignalStorageRecord remote2 = unknown(4);
SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true));
SignalStorageRecord local1 = unknown(1); SignalStorageRecord local1 = unknown(1);
SignalStorageRecord local2 = unknown(2); 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.getLocalContactInserts().isEmpty());
assertTrue(result.getLocalContactUpdates().isEmpty()); assertTrue(result.getLocalContactUpdates().isEmpty());
assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts()); assertEquals(setOf(remote1, remote2), result.getLocalUnknownInserts());
assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes()); assertEquals(setOf(local1, local2), result.getLocalUnknownDeletes());
assertTrue(result.getRemoteDeletes().isEmpty()); assertTrue(result.getRemoteDeletes().isEmpty());
} }
@ -224,29 +254,34 @@ public final class StorageSyncHelperTest {
SignalGroupV1Record remote4 = groupV1(7, 1, true, false); SignalGroupV1Record remote4 = groupV1(7, 1, true, false);
SignalGroupV1Record local4 = groupV1(8, 1, false, true); SignalGroupV1Record local4 = groupV1(8, 1, false, true);
SignalAccountRecord remote5 = account(9); SignalGroupV2Record remote5 = groupV2(9, 2, true, false);
SignalAccountRecord local5 = account(10); SignalGroupV2Record local5 = groupV2(10, 2, false, true);
SignalStorageRecord unknownRemote = unknown(11); SignalAccountRecord remote6 = account(11);
SignalStorageRecord unknownLocal = unknown(12); SignalAccountRecord local6 = account(12);
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222)); SignalStorageRecord unknownRemote = unknown(13);
SignalStorageRecord unknownLocal = unknown(14);
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote); StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222, 333));
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, unknownLocal);
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, remote6, unknownRemote);
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, local6, unknownLocal);
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly); MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a"); SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a");
SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b"); SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b");
SignalGroupV1Record merge4 = groupV1(222, 1, true, true); SignalGroupV1Record merge4 = groupV1(222, 1, true, true);
SignalGroupV2Record merge5 = groupV2(333, 2, true, true);
assertEquals(setOf(remote3), result.getLocalContactInserts()); assertEquals(setOf(remote3), result.getLocalContactInserts());
assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates()); assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates());
assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates()); assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates());
assertEquals(setOf(update(local5, merge5)), result.getLocalGroupV2Updates());
assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts()); assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts());
assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates()); assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4), recordUpdate(remote5, merge5)), result.getRemoteUpdates());
assertEquals(Optional.of(update(local5, remote5)), result.getLocalAccountUpdate()); assertEquals(Optional.of(update(local6, remote6)), result.getLocalAccountUpdate());
assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts()); assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts());
assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes()); assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes());
assertTrue(result.getRemoteDeletes().isEmpty()); assertTrue(result.getRemoteDeletes().isEmpty());
@ -254,7 +289,7 @@ public final class StorageSyncHelperTest {
@Test @Test
public void createWriteOperation_generic() { public void createWriteOperation_generic() {
List<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100)); List<StorageId> 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 insert1 = contact(6, UUID_A, E164_A, "a");
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b"); SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b");
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z"); 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 insert3 = groupV1(9, 1, true, true);
SignalGroupV1Record old3 = groupV1(100, 1, true, true); SignalGroupV1Record old3 = groupV1(100, 1, true, true);
SignalGroupV1Record new3 = groupV1(10, 1, false, 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 unknownInsert = unknown(11);
SignalStorageRecord unknownDelete = unknown(12); SignalStorageRecord unknownDelete = unknown(12);
@ -273,18 +311,19 @@ public final class StorageSyncHelperTest {
setOf(update(old2, new2)), setOf(update(old2, new2)),
setOf(insert3), setOf(insert3),
setOf(update(old3, new3)), setOf(update(old3, new3)),
setOf(insert4),
setOf(update(old4, new4)),
setOf(unknownInsert), setOf(unknownInsert),
setOf(unknownDelete), setOf(unknownDelete),
Optional.absent(), Optional.absent(),
recordSetOf(insert1, insert3), recordSetOf(insert1, insert3, insert4),
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)), setOf(recordUpdate(old1, new1), recordUpdate(old3, new3), recordUpdate(old4, new4)),
setOf())); setOf()));
assertEquals(2, result.getManifest().getVersion()); 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()); 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());
assertTrue(recordSetOf(insert1, new1, insert3, new3).containsAll(result.getInserts())); assertEquals(recordSetOf(insert1, new1, insert3, new3, insert4, new4), new HashSet<>(result.getInserts()));
assertEquals(4, result.getInserts().size()); assertByteListEquals(byteListOf(1, 100, 200), result.getDeletes());
assertByteListEquals(byteListOf(1, 100), result.getDeletes());
} }
@Test @Test
@ -415,6 +454,10 @@ public final class StorageSyncHelperTest {
return StorageId.forGroupV1(byteArray(val)); return StorageId.forGroupV1(byteArray(val));
} }
private static StorageId groupV2Key(int val) {
return StorageId.forGroupV2(byteArray(val));
}
private static StorageId unknownKey(int val) { private static StorageId unknownKey(int val) {
return StorageId.forType(byteArray(val), UNKNOWN_TYPE); return StorageId.forType(byteArray(val), UNKNOWN_TYPE);
} }

View File

@ -69,7 +69,7 @@ public final class SignalGroupV2Record implements SignalRecord {
private final GroupV2Record.Builder builder; private final GroupV2Record.Builder builder;
public Builder(byte[] rawId, GroupMasterKey masterKey) { public Builder(byte[] rawId, GroupMasterKey masterKey) {
this.id = StorageId.forGroupV1(rawId); this.id = StorageId.forGroupV2(rawId);
this.builder = GroupV2Record.newBuilder(); this.builder = GroupV2Record.newBuilder();
builder.setMasterKey(ByteString.copyFrom(masterKey.serialize())); builder.setMasterKey(ByteString.copyFrom(masterKey.serialize()));

View File

@ -7,7 +7,7 @@ import java.util.Objects;
public class SignalStorageRecord implements SignalRecord { public class SignalStorageRecord implements SignalRecord {
private final StorageId id; private final StorageId id;
private final Optional<SignalContactRecord> contact; private final Optional<SignalContactRecord> contact;
private final Optional<SignalGroupV1Record> groupV1; private final Optional<SignalGroupV1Record> groupV1;
private final Optional<SignalGroupV2Record> groupV2; private final Optional<SignalGroupV2Record> groupV2;
@ -88,7 +88,7 @@ public class SignalStorageRecord implements SignalRecord {
} }
public boolean isUnknown() { public boolean isUnknown() {
return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent(); return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent();
} }
@Override @Override
@ -97,12 +97,13 @@ public class SignalStorageRecord implements SignalRecord {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
SignalStorageRecord that = (SignalStorageRecord) o; SignalStorageRecord that = (SignalStorageRecord) o;
return Objects.equals(id, that.id) && return Objects.equals(id, that.id) &&
Objects.equals(contact, that.contact) && Objects.equals(contact, that.contact) &&
Objects.equals(groupV1, that.groupV1); Objects.equals(groupV1, that.groupV1) &&
Objects.equals(groupV2, that.groupV2);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, contact, groupV1); return Objects.hash(id, contact, groupV1, groupV2);
} }
} }