Get authoritative profile keys from group changes only.

This commit is contained in:
Alan Evans
2020-08-03 16:12:02 -03:00
committed by Greyson Parrelli
parent 17c0364eda
commit 26868ae668
6 changed files with 425 additions and 135 deletions

View File

@@ -18,17 +18,11 @@ import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.thoughtcrime.securesms.testutil.MainThreadUtil;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -44,6 +38,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown;
import static org.thoughtcrime.securesms.util.StringUtil.isolateBidi;
@RunWith(RobolectricTestRunner.class)
@@ -932,102 +928,6 @@ public final class GroupsV2UpdateMessageProducerTest {
}
}
private static class ChangeBuilder {
private final DecryptedGroupChange.Builder builder;
ChangeBuilder(@NonNull UUID editor) {
builder = DecryptedGroupChange.newBuilder()
.setEditor(UuidUtil.toByteString(editor));
}
ChangeBuilder() {
builder = DecryptedGroupChange.newBuilder();
}
ChangeBuilder addMember(@NonNull UUID newMember) {
builder.addNewMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(newMember)));
return this;
}
ChangeBuilder deleteMember(@NonNull UUID removedMember) {
builder.addDeleteMembers(UuidUtil.toByteString(removedMember));
return this;
}
ChangeBuilder promoteToAdmin(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
ChangeBuilder demoteToMember(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
ChangeBuilder invite(@NonNull UUID potentialMember) {
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(potentialMember)));
return this;
}
ChangeBuilder uninvite(@NonNull UUID pendingMember) {
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(pendingMember)));
return this;
}
ChangeBuilder promote(@NonNull UUID pendingMember) {
builder.addPromotePendingMembers(DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(pendingMember)));
return this;
}
ChangeBuilder title(@NonNull String newTitle) {
builder.setNewTitle(DecryptedString.newBuilder()
.setValue(newTitle));
return this;
}
ChangeBuilder avatar(@NonNull String newAvatar) {
builder.setNewAvatar(DecryptedString.newBuilder()
.setValue(newAvatar));
return this;
}
ChangeBuilder timer(int duration) {
builder.setNewTimer(DecryptedTimer.newBuilder()
.setDuration(duration));
return this;
}
ChangeBuilder attributeAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewAttributeAccess(accessRequired);
return this;
}
ChangeBuilder membershipAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewMemberAccess(accessRequired);
return this;
}
DecryptedGroupChange build() {
return builder.build();
}
}
private static ChangeBuilder changeBy(@NonNull UUID groupEditor) {
return new ChangeBuilder(groupEditor);
}
private static ChangeBuilder changeByUnknown() {
return new ChangeBuilder();
}
private static @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map<UUID, String> map) {
return uuid -> {
String name = map.get(uuid);

View File

@@ -0,0 +1,140 @@
package org.thoughtcrime.securesms.groups.v2;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
public final class ChangeBuilder {
private final DecryptedGroupChange.Builder builder;
public static ChangeBuilder changeBy(@NonNull UUID editor) {
return new ChangeBuilder(editor);
}
public static ChangeBuilder changeByUnknown() {
return new ChangeBuilder();
}
ChangeBuilder(@NonNull UUID editor) {
builder = DecryptedGroupChange.newBuilder()
.setEditor(UuidUtil.toByteString(editor));
}
ChangeBuilder() {
builder = DecryptedGroupChange.newBuilder();
}
public ChangeBuilder addMember(@NonNull UUID newMember) {
builder.addNewMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(newMember)));
return this;
}
public ChangeBuilder addMember(@NonNull UUID newMember, @NonNull ProfileKey profileKey) {
builder.addNewMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(newMember))
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
return this;
}
public ChangeBuilder deleteMember(@NonNull UUID removedMember) {
builder.addDeleteMembers(UuidUtil.toByteString(removedMember));
return this;
}
public ChangeBuilder promoteToAdmin(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
public ChangeBuilder demoteToMember(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
public ChangeBuilder invite(@NonNull UUID potentialMember) {
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(potentialMember)));
return this;
}
public ChangeBuilder uninvite(@NonNull UUID pendingMember) {
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(pendingMember)));
return this;
}
public ChangeBuilder promote(@NonNull UUID pendingMember) {
builder.addPromotePendingMembers(DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(pendingMember)));
return this;
}
public ChangeBuilder profileKeyUpdate(@NonNull UUID member, @NonNull ProfileKey profileKey) {
return profileKeyUpdate(member, profileKey.serialize());
}
public ChangeBuilder profileKeyUpdate(@NonNull UUID member, @NonNull byte[] profileKey) {
builder.addModifiedProfileKeys(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(member))
.setProfileKey(ByteString.copyFrom(profileKey)));
return this;
}
public ChangeBuilder promote(@NonNull UUID pendingMember, @NonNull ProfileKey profileKey) {
builder.addPromotePendingMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(pendingMember))
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
return this;
}
public ChangeBuilder title(@NonNull String newTitle) {
builder.setNewTitle(DecryptedString.newBuilder()
.setValue(newTitle));
return this;
}
public ChangeBuilder avatar(@NonNull String newAvatar) {
builder.setNewAvatar(DecryptedString.newBuilder()
.setValue(newAvatar));
return this;
}
public ChangeBuilder timer(int duration) {
builder.setNewTimer(DecryptedTimer.newBuilder()
.setDuration(duration));
return this;
}
public ChangeBuilder attributeAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewAttributeAccess(accessRequired);
return this;
}
public ChangeBuilder membershipAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewMemberAccess(accessRequired);
return this;
}
public DecryptedGroupChange build() {
return builder.build();
}
}

View File

@@ -0,0 +1,182 @@
package org.thoughtcrime.securesms.groups.v2;
import org.junit.Test;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.testutil.LogRecorder;
import java.util.Collection;
import java.util.UUID;
import edu.emory.mathcs.backport.java.util.Collections;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown;
import static org.thoughtcrime.securesms.testutil.LogRecorder.hasMessages;
public final class ProfileKeySetTest {
@Test
public void empty_change() {
UUID editor = UUID.randomUUID();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
}
@Test
public void new_member_is_not_authoritative() {
UUID editor = UUID.randomUUID();
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).addMember(newMember, profileKey).build());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
}
@Test
public void new_member_by_self_is_authoritative() {
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(newMember).addMember(newMember, profileKey).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
}
@Test
public void new_member_by_self_promote_is_authoritative() {
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(newMember).promote(newMember, profileKey).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
}
@Test
public void new_member_by_promote_by_other_editor_is_not_authoritative() {
UUID editor = UUID.randomUUID();
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).promote(newMember, profileKey).build());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
}
@Test
public void new_member_by_promote_by_unknown_editor_is_not_authoritative() {
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeByUnknown().promote(newMember, profileKey).build());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
}
@Test
public void profile_key_update_by_self_is_authoritative() {
UUID member = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey)));
}
@Test
public void profile_key_update_by_another_is_not_authoritative() {
UUID editor = UUID.randomUUID();
UUID member = UUID.randomUUID();
ProfileKey profileKey = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey).build());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(member, profileKey)));
}
@Test
public void multiple_updates_overwrite() {
UUID editor = UUID.randomUUID();
UUID member = UUID.randomUUID();
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey1).build());
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey2).build());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(member, profileKey2)));
}
@Test
public void authoritative_takes_priority_when_seen_first() {
UUID editor = UUID.randomUUID();
UUID member = UUID.randomUUID();
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey1).build());
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey2).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey1)));
}
@Test
public void authoritative_takes_priority_when_seen_second() {
UUID editor = UUID.randomUUID();
UUID member = UUID.randomUUID();
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
ProfileKeySet profileKeySet = new ProfileKeySet();
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey1).build());
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey2).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey2)));
}
@Test
public void bad_profile_key() {
LogRecorder logRecorder = new LogRecorder();
UUID editor = UUID.randomUUID();
UUID member = UUID.randomUUID();
byte[] badProfileKey = new byte[10];
ProfileKeySet profileKeySet = new ProfileKeySet();
Log.initialize(logRecorder);
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, badProfileKey).build());
assertTrue(profileKeySet.getProfileKeys().isEmpty());
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
assertThat(logRecorder.getWarnings(), hasMessages("Bad profile key in group"));
}
}

View File

@@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.testutil;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
@@ -95,4 +98,46 @@ public final class LogRecorder extends Log.Logger {
return throwable;
}
}
@SafeVarargs
public static <T> Matcher<T> hasMessages(T... messages) {
return new BaseMatcher<T>() {
@Override
public void describeTo(Description description) {
description.appendValueList("[", ", ", "]", messages);
}
@Override
public void describeMismatch(Object item, Description description) {
@SuppressWarnings("unchecked")
List<Entry> list = (List<Entry>) item;
ArrayList<String> messages = new ArrayList<>(list.size());
for (Entry e : list) {
messages.add(e.message);
}
description.appendText("was ").appendValueList("[", ", ", "]", messages);
}
@Override
public boolean matches(Object item) {
@SuppressWarnings("unchecked")
List<Entry> list = (List<Entry>) item;
if (list.size() != messages.length) {
return false;
}
for (int i = 0; i < messages.length; i++) {
if (!list.get(i).message.equals(messages[i])) {
return false;
}
}
return true;
}
};
}
}