mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 19:56:02 +01:00
Get authoritative profile keys from group changes only.
This commit is contained in:
committed by
Greyson Parrelli
parent
17c0364eda
commit
26868ae668
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user