Add interim storage support for GroupV2Record.

This commit is contained in:
Greyson Parrelli
2020-03-18 12:37:28 -04:00
parent 707a2aca0a
commit 7a038ab09d
10 changed files with 311 additions and 11 deletions

View File

@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.util.Collection;
import java.util.Map;
class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGroupV2Record> {
private final Map<GroupMasterKey, SignalGroupV2Record> localByGroupId;
GroupV2ConflictMerger(@NonNull Collection<SignalGroupV2Record> localOnly) {
localByGroupId = Stream.of(localOnly).collect(Collectors.toMap(SignalGroupV2Record::getMasterKey, g -> g));
}
@Override
public @NonNull Optional<SignalGroupV2Record> getMatching(@NonNull SignalGroupV2Record record) {
return Optional.fromNullable(localByGroupId.get(record.getMasterKey()));
}
@Override
public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
boolean blocked = remote.isBlocked();
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
boolean archived = remote.isArchived();
boolean matchesRemote = blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived();
boolean matchesLocal = blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived();
if (matchesRemote) {
return remote;
} else if (matchesLocal) {
return local;
} else {
return new SignalGroupV2Record.Builder(keyGenerator.generate(), remote.getMasterKey())
.setBlocked(blocked)
.setProfileSharingEnabled(blocked)
.build();
}
}
}

View File

@@ -139,8 +139,9 @@ 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<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).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();
RecordMergeResult<SignalContactRecord, RecordUpdate<SignalContactRecord>> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts));
RecordMergeResult<SignalGroupV1Record, RecordUpdate<SignalGroupV1Record>> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.storage;
import org.junit.Test;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyGenerator;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.util.Collections;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
public class GroupV2ConflictMergerTest {
private static byte[] GENERATED_KEY = byteArray(8675309);
private static KeyGenerator KEY_GENERATOR = mock(KeyGenerator.class);
static {
when(KEY_GENERATOR.generate()).thenReturn(GENERATED_KEY);
}
@Test
public void merge_alwaysPreferRemote_exceptProfileSharingIsEitherOr() {
SignalGroupV2Record remote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(100))
.setBlocked(false)
.setProfileSharingEnabled(false)
.setArchived(false)
.build();
SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100))
.setBlocked(true)
.setProfileSharingEnabled(true)
.setArchived(true)
.build();
SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, KEY_GENERATOR);
assertArrayEquals(GENERATED_KEY, merged.getId().getRaw());
assertEquals(groupKey(100), merged.getMasterKey());
assertFalse(merged.isBlocked());
assertFalse(merged.isArchived());
}
@Test
public void merge_returnRemoteIfEndResultMatchesRemote() {
SignalGroupV2Record remote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(100))
.setBlocked(false)
.setProfileSharingEnabled(true)
.setArchived(true)
.build();
SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100))
.setBlocked(true)
.setProfileSharingEnabled(false)
.setArchived(false)
.build();
SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
assertEquals(remote, merged);
}
@Test
public void merge_returnLocalIfEndResultMatchesLocal() {
SignalGroupV2Record remote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(100))
.setBlocked(false)
.setProfileSharingEnabled(false)
.setArchived(false)
.build();
SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100))
.setBlocked(false)
.setProfileSharingEnabled(true)
.setArchived(false)
.build();
SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
assertEquals(local, merged);
}
private static GroupMasterKey groupKey(int value) {
try {
return new GroupMasterKey(byteArray(value, 32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -6,12 +6,14 @@ import com.annimon.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
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.SignalRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
@@ -151,15 +153,17 @@ public final class StorageSyncHelperTest {
public void resolveConflict_unknowns() {
SignalStorageRecord remote1 = unknown(3);
SignalStorageRecord remote2 = unknown(4);
SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true));
SignalStorageRecord local1 = unknown(1);
SignalStorageRecord local2 = unknown(2);
SignalStorageRecord local3 = SignalStorageRecord.forGroupV2(groupV2(101, 201, true, true));
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2), setOf(local1, local2));
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3), setOf(local1, local2, local3));
assertTrue(result.getLocalContactInserts().isEmpty());
assertTrue(result.getLocalContactUpdates().isEmpty());
assertEquals(setOf(remote1, remote2), result.getLocalUnknownInserts());
assertEquals(setOf(local1, local2), result.getLocalUnknownDeletes());
assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts());
assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes());
}
@Test
@@ -269,6 +273,8 @@ public final class StorageSyncHelperTest {
storageRecords.add(SignalStorageRecord.forContact(record.getId(), (SignalContactRecord) record));
} else if (record instanceof SignalGroupV1Record) {
storageRecords.add(SignalStorageRecord.forGroupV1(record.getId(), (SignalGroupV1Record) record));
} else if (record instanceof SignalGroupV2Record) {
storageRecords.add(SignalStorageRecord.forGroupV2(record.getId(), (SignalGroupV2Record) record));
} else {
storageRecords.add(SignalStorageRecord.forUnknown(record.getId()));
}
@@ -312,6 +318,18 @@ public final class StorageSyncHelperTest {
return new SignalGroupV1Record.Builder(byteArray(key), byteArray(groupId)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
}
private static SignalGroupV2Record groupV2(int key,
int groupId,
boolean blocked,
boolean profileSharing)
{
try {
return new SignalGroupV2Record.Builder(byteArray(key), new GroupMasterKey(byteArray(groupId, 32))).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
private static StorageSyncHelper.RecordUpdate<SignalContactRecord> contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
return new StorageSyncHelper.RecordUpdate<>(oldContact, newContact);
}

View File

@@ -4,6 +4,7 @@ import com.annimon.stream.Stream;
import com.google.common.collect.Sets;
import org.thoughtcrime.securesms.util.Conversions;
import org.whispersystems.libsignal.util.ByteUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -23,6 +24,12 @@ public final class TestHelpers {
return Conversions.intToByteArray(a);
}
public static byte[] byteArray(int a, int totalLength) {
byte[] out = new byte[totalLength - 4];
byte[] val = Conversions.intToByteArray(a);
return ByteUtil.combine(out, val);
}
public static List<byte[]> byteListOf(int... vals) {
List<byte[]> list = new ArrayList<>(vals.length);