Get authoritative profile keys from group changes only.
parent
17c0364eda
commit
26868ae668
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
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.DecryptedMember;
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
@ -30,38 +31,62 @@ public final class ProfileKeySet {
|
||||||
private final Map<UUID, ProfileKey> authoritativeProfileKeys = new LinkedHashMap<>();
|
private final Map<UUID, ProfileKey> authoritativeProfileKeys = new LinkedHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new profile keys from the group state.
|
* Add new profile keys from a group change.
|
||||||
|
* <p>
|
||||||
|
* If the change came from the member whose profile key is changing then it is regarded as
|
||||||
|
* authoritative.
|
||||||
*/
|
*/
|
||||||
public void addKeysFromGroupState(@NonNull DecryptedGroup group,
|
public void addKeysFromGroupChange(@NonNull DecryptedGroupChange change) {
|
||||||
@Nullable UUID changeSource)
|
UUID editor = UuidUtil.fromByteStringOrNull(change.getEditor());
|
||||||
{
|
|
||||||
|
for (DecryptedMember member : change.getNewMembersList()) {
|
||||||
|
addMemberKey(member, editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedMember member : change.getPromotePendingMembersList()) {
|
||||||
|
addMemberKey(member, editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedMember member : change.getModifiedProfileKeysList()) {
|
||||||
|
addMemberKey(member, editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new profile keys from the group state.
|
||||||
|
* <p>
|
||||||
|
* Profile keys found in group state are never authoritative as the change cannot be easily
|
||||||
|
* attributed to a member and it's possible that the group is out of date. So profile keys
|
||||||
|
* gathered from a group state can only be used to fill in gaps in knowledge.
|
||||||
|
*/
|
||||||
|
public void addKeysFromGroupState(@NonNull DecryptedGroup group) {
|
||||||
for (DecryptedMember member : group.getMembersList()) {
|
for (DecryptedMember member : group.getMembersList()) {
|
||||||
UUID memberUuid = UuidUtil.fromByteString(member.getUuid());
|
addMemberKey(member, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) {
|
private void addMemberKey(@NonNull DecryptedMember member, @Nullable UUID changeSource) {
|
||||||
Log.w(TAG, "Seen unknown member UUID");
|
UUID memberUuid = UuidUtil.fromByteString(member.getUuid());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileKey profileKey;
|
if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) {
|
||||||
try {
|
Log.w(TAG, "Seen unknown member UUID");
|
||||||
profileKey = new ProfileKey(member.getProfileKey().toByteArray());
|
return;
|
||||||
} catch (InvalidInputException e) {
|
}
|
||||||
Log.w(TAG, "Bad profile key in group");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeSource != null) {
|
ProfileKey profileKey;
|
||||||
Log.d(TAG, String.format("Change %s by %s", memberUuid, changeSource));
|
try {
|
||||||
|
profileKey = new ProfileKey(member.getProfileKey().toByteArray());
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
Log.w(TAG, "Bad profile key in group");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (changeSource.equals(memberUuid)) {
|
if (memberUuid.equals(changeSource)) {
|
||||||
authoritativeProfileKeys.put(memberUuid, profileKey);
|
authoritativeProfileKeys.put(memberUuid, profileKey);
|
||||||
profileKeys.remove(memberUuid);
|
profileKeys.remove(memberUuid);
|
||||||
} else {
|
} else {
|
||||||
if (!authoritativeProfileKeys.containsKey(memberUuid)) {
|
if (!authoritativeProfileKeys.containsKey(memberUuid)) {
|
||||||
profileKeys.put(memberUuid, profileKey);
|
profileKeys.put(memberUuid, profileKey);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
|
||||||
|
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||||
|
@ -16,8 +14,6 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
import org.signal.zkgroup.util.UUIDUtil;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
|
@ -320,9 +316,11 @@ public final class GroupsV2StateProcessor {
|
||||||
final ProfileKeySet profileKeys = new ProfileKeySet();
|
final ProfileKeySet profileKeys = new ProfileKeySet();
|
||||||
|
|
||||||
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
|
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
|
||||||
Optional<UUID> editor = DecryptedGroupUtil.editorUuid(entry.getChange());
|
if (entry.getGroup() != null) {
|
||||||
if (editor.isPresent() && entry.getGroup() != null) {
|
profileKeys.addKeysFromGroupState(entry.getGroup());
|
||||||
profileKeys.addKeysFromGroupState(entry.getGroup(), editor.get());
|
}
|
||||||
|
if (entry.getChange() != null) {
|
||||||
|
profileKeys.addKeysFromGroupChange(entry.getChange());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,11 @@ import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
import org.signal.storageservice.protos.groups.AccessControl;
|
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.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
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.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.testutil.MainThreadUtil;
|
||||||
import org.thoughtcrime.securesms.util.StringUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
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.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
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;
|
import static org.thoughtcrime.securesms.util.StringUtil.isolateBidi;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@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) {
|
private static @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map<UUID, String> map) {
|
||||||
return uuid -> {
|
return uuid -> {
|
||||||
String name = map.get(uuid);
|
String name = map.get(uuid);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package org.thoughtcrime.securesms.testutil;
|
package org.thoughtcrime.securesms.testutil;
|
||||||
|
|
||||||
|
import org.hamcrest.BaseMatcher;
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -95,4 +98,46 @@ public final class LogRecorder extends Log.Logger {
|
||||||
return throwable;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue