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 */
/** 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 */

View File

@@ -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<String> 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<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() {
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<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() };
return getRecipientSettings(query, args);
return getRecipientSettingsForSync(query, args);
}
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() };
return getRecipientSettings(query, args);
return getRecipientSettingsForSync(query, args);
}
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) {
return result.get(0);
@@ -481,7 +505,9 @@ public class RecipientDatabase extends Database {
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
@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();
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<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();
} 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<RecipientSettings> getRecipientSettings(@Nullable String query, @Nullable String[] args) {
private List<RecipientSettings> 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<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()) {
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;
}

View File

@@ -85,7 +85,7 @@ public class StorageForcePushJob extends BaseJob {
Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> 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();

View File

@@ -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());
}

View File

@@ -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<RecipientId, byte[]> 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<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<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(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).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();
@@ -242,11 +254,13 @@ public final class StorageSyncHelper {
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
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))));
Set<SignalStorageRecord> 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<RecordUpdate<SignalStorageRecord>> 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<RecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts;
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> localUnknownDeletes;
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
@@ -461,6 +482,8 @@ public final class StorageSyncHelper {
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
@NonNull Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Optional<RecordUpdate<SignalAccountRecord>> 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<RecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
return localGroupV1Updates;
}
public @NonNull Set<SignalGroupV2Record> getLocalGroupV2Inserts() {
return localGroupV2Inserts;
}
public @NonNull Set<RecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
return localGroupV2Updates;
}
public @NonNull Set<SignalStorageRecord> 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());
}
}

View File

@@ -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<RecipientId> 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<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())
.setProfileSharingEnabled(recipient.isProfileSharing())
.setArchived(archived.contains(recipient.getId()))