Group invite link epoch support.
This commit is contained in:
parent
e006306036
commit
477bb45df7
31 changed files with 2366 additions and 205 deletions
|
@ -9,12 +9,14 @@ 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.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
|
@ -93,6 +95,10 @@ final class GroupsV2UpdateMessageProducer {
|
|||
describeUnknownEditorNewTimer(change, updates);
|
||||
describeUnknownEditorNewAttributeAccess(change, updates);
|
||||
describeUnknownEditorNewMembershipAccess(change, updates);
|
||||
describeUnknownEditorNewGroupInviteLinkAccess(change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeUnknownEditorRequestingMembersApprovals(change, updates);
|
||||
describeUnknownEditorRequestingMembersDeletes(change, updates);
|
||||
|
||||
describeUnknownEditorMemberRemovals(change, updates);
|
||||
|
||||
|
@ -112,6 +118,10 @@ final class GroupsV2UpdateMessageProducer {
|
|||
describeNewTimer(change, updates);
|
||||
describeNewAttributeAccess(change, updates);
|
||||
describeNewMembershipAccess(change, updates);
|
||||
describeNewGroupInviteLinkAccess(change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeRequestingMembersApprovals(change, updates);
|
||||
describeRequestingMembersDeletes(change, updates);
|
||||
|
||||
describeMemberRemovals(change, updates);
|
||||
|
||||
|
@ -148,7 +158,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
}
|
||||
|
@ -157,7 +167,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_sharable_group_link, newMember)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
}
|
||||
|
@ -498,6 +508,123 @@ final class GroupsV2UpdateMessageProducer {
|
|||
}
|
||||
}
|
||||
|
||||
private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
boolean groupLinkEnabled = false;
|
||||
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link, editor)));
|
||||
}
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link_with_admin_approval)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link_with_admin_approval, editor)));
|
||||
}
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_sharable_group_link, editor)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_sharable_group_link, editor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on)));
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on_with_admin_approval)));
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_off)));
|
||||
break;
|
||||
}
|
||||
|
||||
if (change.getNewInviteLinkPassword().size() > 0) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_reset)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembers(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_sharable_group_link, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved)));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DescribeMemberStrategy {
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,7 @@ public final class GV2AccessLevelUtil {
|
|||
|
||||
public static String toString(@NonNull Context context, @NonNull AccessControl.AccessRequired attributeAccess) {
|
||||
switch (attributeAccess) {
|
||||
case ANY : return context.getString(R.string.GroupManagement_access_level_anyone);
|
||||
case MEMBER : return context.getString(R.string.GroupManagement_access_level_all_members);
|
||||
case ADMINISTRATOR : return context.getString(R.string.GroupManagement_access_level_only_admins);
|
||||
default : return context.getString(R.string.GroupManagement_access_level_unknown);
|
||||
|
|
|
@ -8,7 +8,7 @@ import com.google.protobuf.ByteString;
|
|||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
|
|
|
@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.groups.v2;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -50,6 +53,10 @@ public final class ProfileKeySet {
|
|||
for (DecryptedMember member : change.getModifiedProfileKeysList()) {
|
||||
addMemberKey(member, editor);
|
||||
}
|
||||
|
||||
for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) {
|
||||
addMemberKey(editor, member.getUuid(), member.getProfileKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +73,14 @@ public final class ProfileKeySet {
|
|||
}
|
||||
|
||||
private void addMemberKey(@NonNull DecryptedMember member, @Nullable UUID changeSource) {
|
||||
UUID memberUuid = UuidUtil.fromByteString(member.getUuid());
|
||||
addMemberKey(changeSource, member.getUuid(), member.getProfileKey());
|
||||
}
|
||||
|
||||
private void addMemberKey(@Nullable UUID changeSource,
|
||||
@NonNull ByteString memberUuidBytes,
|
||||
@NonNull ByteString profileKeyBytes)
|
||||
{
|
||||
UUID memberUuid = UuidUtil.fromByteString(memberUuidBytes);
|
||||
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) {
|
||||
Log.w(TAG, "Seen unknown member UUID");
|
||||
|
@ -75,7 +89,7 @@ public final class ProfileKeySet {
|
|||
|
||||
ProfileKey profileKey;
|
||||
try {
|
||||
profileKey = new ProfileKey(member.getProfileKey().toByteArray());
|
||||
profileKey = new ProfileKey(profileKeyBytes.toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Bad profile key in group");
|
||||
return;
|
||||
|
|
|
@ -463,6 +463,7 @@
|
|||
<string name="GroupMembersDialog_you">You</string>
|
||||
|
||||
<!-- GV2 access levels -->
|
||||
<string name="GroupManagement_access_level_anyone">Anyone</string>
|
||||
<string name="GroupManagement_access_level_all_members">All members</string>
|
||||
<string name="GroupManagement_access_level_only_admins">Only admins</string>
|
||||
<string name="GroupManagement_access_level_unknown" translatable="false">Unknown</string>
|
||||
|
@ -914,6 +915,41 @@
|
|||
<string name="MessageRecord_s_changed_who_can_edit_group_membership_to_s">%1$s changed who can edit group membership to \"%2$s\".</string>
|
||||
<string name="MessageRecord_who_can_edit_group_membership_has_been_changed_to_s">Who can edit group membership has been changed to \"%1$s\".</string>
|
||||
|
||||
<!-- GV2 group link invite access level change -->
|
||||
<string name="MessageRecord_you_turned_on_the_sharable_group_link">You turned on the sharable group link.</string>
|
||||
<string name="MessageRecord_you_turned_on_the_sharable_group_link_with_admin_approval">You turned on the sharable group link with admin approval.</string>
|
||||
<string name="MessageRecord_you_turned_off_the_sharable_group_link">You turned off the sharable group link.</string>
|
||||
<string name="MessageRecord_s_turned_on_the_sharable_group_link">%1$s turned on the sharable group link.</string>
|
||||
<string name="MessageRecord_s_turned_on_the_sharable_group_link_with_admin_approval">%1$s turned on the sharable group link with admin approval.</string>
|
||||
<string name="MessageRecord_s_turned_off_the_sharable_group_link">%1$s turned off the sharable group link.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_on">The sharable group link has been turned on.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_on_with_admin_approval">The sharable group link has been turned on with admin approval.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_off">The sharable group link has been turned off.</string>
|
||||
|
||||
<!-- GV2 group link reset -->
|
||||
<string name="MessageRecord_you_reset_the_sharable_group_link">You reset the sharable group link.</string>
|
||||
<string name="MessageRecord_s_reset_the_sharable_group_link">%1$s reset the sharable group link.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_reset">The sharable group link has been reset.</string>
|
||||
|
||||
<!-- GV2 group link joins -->
|
||||
<string name="MessageRecord_you_joined_the_group_via_the_sharable_group_link">You joined the group via the sharable group link.</string>
|
||||
<string name="MessageRecord_s_joined_the_group_via_the_sharable_group_link">%1$s joined the group via the sharable group link.</string>
|
||||
|
||||
<!-- GV2 group link requests -->
|
||||
<string name="MessageRecord_you_sent_a_request_to_join_the_group">You sent a request to join the group.</string>
|
||||
<string name="MessageRecord_s_requested_to_join_via_the_sharable_group_link">%1$s requested to join via the sharable group link.</string>
|
||||
|
||||
<!-- GV2 group link approvals -->
|
||||
<string name="MessageRecord_s_approved_your_request_to_join_the_group">%1$s approved your request to join the group.</string>
|
||||
<string name="MessageRecord_s_approved_a_request_to_join_the_group_from_s">%1$s approved a request to join the group from %2$s.</string>
|
||||
<string name="MessageRecord_your_request_to_join_the_group_has_been_approved">Your request to join the group has been approved.</string>
|
||||
<string name="MessageRecord_a_request_to_join_the_group_from_s_has_been_approved">A request to join the group from %1$s has been approved.</string>
|
||||
|
||||
<!-- GV2 group link deny -->
|
||||
<string name="MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin">Your request to join the group has been denied by an admin.</string>
|
||||
<string name="MessageRecord_s_denied_a_request_to_join_the_group_from_s">%1$s denied a request to join the group from %2$s.</string>
|
||||
<string name="MessageRecord_a_request_to_join_the_group_from_s_has_been_denied">A request to join the group from %1$s has been denied.</string>
|
||||
|
||||
<!-- End of GV2 specific update messages -->
|
||||
|
||||
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
|
||||
|
|
|
@ -143,7 +143,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -152,7 +152,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -209,7 +209,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(Arrays.asList("You joined the group.", "You added Alice.")));
|
||||
assertThat(describeChange(change), is(Arrays.asList("You joined the group via the sharable group link.", "You added Alice.")));
|
||||
}
|
||||
|
||||
// Member removals
|
||||
|
@ -838,6 +838,257 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
assertThat(describeChange(change), is(singletonList("Who can edit group membership has been changed to \"Only admins\".")));
|
||||
}
|
||||
|
||||
// Group link access change
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned off the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned off the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned off.")));
|
||||
}
|
||||
|
||||
// Group link reset
|
||||
|
||||
@Test
|
||||
public void you_reset_group_link() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You reset the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_reset_group_link() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice reset the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_reset_group_link() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been reset.")));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the group link is turned on and reset in the same change, assume this is the first time
|
||||
* the link password it being set and do not show reset message.
|
||||
*/
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_on_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the group link is turned on and reset in the same change, assume this is the first time
|
||||
* the link password it being set and do not show reset message.
|
||||
*/
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_on_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_off_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(Arrays.asList("You turned off the sharable group link.", "You reset the sharable group link.")));
|
||||
}
|
||||
|
||||
// Group link request
|
||||
|
||||
@Test
|
||||
public void you_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.requestJoin()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You sent a request to join the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.requestJoin()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob requested to join via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.requestJoin(alice)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice requested to join via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_approved_your_join_request() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.approveRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob approved your request to join the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_approved_another_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.approveRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice approved a request to join the group from Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_approved_your_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.approveRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been approved.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_approved_another_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.approveRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been approved.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_denied_another_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.denyRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice denied a request to join the group from Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_denied_your_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.denyRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_denied_your_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.denyRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_denied_another_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.denyRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been denied.")));
|
||||
}
|
||||
|
||||
// Multiple changes
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ChangeBuilder {
|
||||
|
||||
private final DecryptedGroupChange.Builder builder;
|
||||
private final DecryptedGroupChange.Builder builder;
|
||||
@Nullable private final UUID editor;
|
||||
|
||||
public static ChangeBuilder changeBy(@NonNull UUID editor) {
|
||||
return new ChangeBuilder(editor);
|
||||
|
@ -31,12 +37,14 @@ public final class ChangeBuilder {
|
|||
}
|
||||
|
||||
ChangeBuilder(@NonNull UUID editor) {
|
||||
builder = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(UuidUtil.toByteString(editor));
|
||||
this.editor = editor;
|
||||
this.builder = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(UuidUtil.toByteString(editor));
|
||||
}
|
||||
|
||||
ChangeBuilder() {
|
||||
builder = DecryptedGroupChange.newBuilder();
|
||||
this.editor = null;
|
||||
this.builder = DecryptedGroupChange.newBuilder();
|
||||
}
|
||||
|
||||
public ChangeBuilder addMember(@NonNull UUID newMember) {
|
||||
|
@ -139,7 +147,58 @@ public final class ChangeBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder inviteLinkAccess(@NonNull AccessControl.AccessRequired accessRequired) {
|
||||
builder.setNewInviteLinkAccess(accessRequired);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder resetGroupLink() {
|
||||
builder.setNewInviteLinkPassword(ByteString.copyFrom(GroupLinkPassword.createNew().serialize()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin() {
|
||||
if (editor == null) throw new AssertionError();
|
||||
return requestJoin(editor, newProfileKey());
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull UUID requester) {
|
||||
return requestJoin(requester, newProfileKey());
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull ProfileKey profileKey) {
|
||||
if (editor == null) throw new AssertionError();
|
||||
return requestJoin(editor, profileKey);
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull UUID requester, @NonNull ProfileKey profileKey) {
|
||||
builder.addNewRequestingMembers(DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(requester))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder approveRequest(@NonNull UUID approvedMember) {
|
||||
builder.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(UuidUtil.toByteString(approvedMember)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder denyRequest(@NonNull UUID approvedMember) {
|
||||
builder.addDeleteRequestingMembers(UuidUtil.toByteString(approvedMember));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DecryptedGroupChange build() {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static ProfileKey newProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import org.junit.Test;
|
|||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
|
@ -6,13 +6,11 @@ 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;
|
||||
|
@ -179,4 +177,29 @@ public final class ProfileKeySetTest {
|
|||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(logRecorder.getWarnings(), hasMessages("Bad profile key in group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_editor_is_authoritative() {
|
||||
UUID editor = UUID.randomUUID();
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(profileKey).build());
|
||||
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(editor, profileKey)));
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_not_editor_is_not_authoritative() {
|
||||
UUID editor = UUID.randomUUID();
|
||||
UUID requesting = UUID.randomUUID();
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(requesting, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(requesting, profileKey)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ dependencies {
|
|||
api 'org.signal:zkgroup-java:0.7.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,14 @@ 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.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
@ -26,8 +28,6 @@ public final class DecryptedGroupUtil {
|
|||
|
||||
private static final String TAG = DecryptedGroupUtil.class.getSimpleName();
|
||||
|
||||
static final int MAX_CHANGE_FIELD = 14;
|
||||
|
||||
public static ArrayList<UUID> toUuidList(Collection<DecryptedMember> membersList) {
|
||||
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
||||
|
||||
|
@ -256,6 +256,16 @@ public final class DecryptedGroupUtil {
|
|||
|
||||
applyModifyMembersAccessControlAction(builder, change);
|
||||
|
||||
applyModifyAddFromInviteLinkAccessControlAction(builder, change);
|
||||
|
||||
applyAddRequestingMembers(builder, change.getNewRequestingMembersList());
|
||||
|
||||
applyDeleteRequestingMembers(builder, change.getDeleteRequestingMembersList());
|
||||
|
||||
applyPromoteRequestingMemberActions(builder, change.getPromoteRequestingMembersList());
|
||||
|
||||
applyInviteLinkPassword(builder, change);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -286,11 +296,12 @@ public final class DecryptedGroupUtil {
|
|||
throw new NotAbleToApplyGroupV2ChangeException();
|
||||
}
|
||||
|
||||
if (modifyMemberRole.getRole() != Member.Role.ADMINISTRATOR && modifyMemberRole.getRole() != Member.Role.DEFAULT) {
|
||||
throw new NotAbleToApplyGroupV2ChangeException();
|
||||
}
|
||||
Member.Role role = modifyMemberRole.getRole();
|
||||
|
||||
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index)).setRole(modifyMemberRole.getRole()).build());
|
||||
ensureKnownRole(role);
|
||||
|
||||
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index))
|
||||
.setRole(role));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -366,18 +377,74 @@ public final class DecryptedGroupUtil {
|
|||
}
|
||||
|
||||
protected static void applyModifyAttributesAccessControlAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
AccessControl.AccessRequired newAccessLevel = change.getNewAttributeAccess();
|
||||
|
||||
if (newAccessLevel != AccessControl.AccessRequired.UNKNOWN) {
|
||||
builder.setAccessControl(AccessControl.newBuilder(builder.getAccessControl())
|
||||
.setAttributesValue(change.getNewAttributeAccessValue())
|
||||
.build());
|
||||
.setAttributesValue(change.getNewAttributeAccessValue()));
|
||||
}
|
||||
}
|
||||
|
||||
protected static void applyModifyMembersAccessControlAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
AccessControl.AccessRequired newAccessLevel = change.getNewMemberAccess();
|
||||
|
||||
if (newAccessLevel != AccessControl.AccessRequired.UNKNOWN) {
|
||||
builder.setAccessControl(AccessControl.newBuilder(builder.getAccessControl())
|
||||
.setMembersValue(change.getNewMemberAccessValue())
|
||||
.build());
|
||||
.setMembersValue(change.getNewMemberAccessValue()));
|
||||
}
|
||||
}
|
||||
|
||||
protected static void applyModifyAddFromInviteLinkAccessControlAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
AccessControl.AccessRequired newAccessLevel = change.getNewInviteLinkAccess();
|
||||
|
||||
if (newAccessLevel != AccessControl.AccessRequired.UNKNOWN) {
|
||||
builder.setAccessControl(AccessControl.newBuilder(builder.getAccessControl())
|
||||
.setAddFromInviteLink(newAccessLevel));
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyAddRequestingMembers(DecryptedGroup.Builder builder, List<DecryptedRequestingMember> newRequestingMembers) {
|
||||
builder.addAllRequestingMembers(newRequestingMembers);
|
||||
}
|
||||
|
||||
private static void applyDeleteRequestingMembers(DecryptedGroup.Builder builder, List<ByteString> deleteRequestingMembersList) {
|
||||
for (ByteString removedMember : deleteRequestingMembersList) {
|
||||
int index = indexOfUuidInRequestingList(builder.getRequestingMembersList(), removedMember);
|
||||
|
||||
if (index == -1) {
|
||||
Log.w(TAG, "Deleted member on change not found in group");
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.removeRequestingMembers(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyPromoteRequestingMemberActions(DecryptedGroup.Builder builder, List<DecryptedApproveMember> promoteRequestingMembers) throws NotAbleToApplyGroupV2ChangeException {
|
||||
for (DecryptedApproveMember approvedMember : promoteRequestingMembers) {
|
||||
int index = indexOfUuidInRequestingList(builder.getRequestingMembersList(), approvedMember.getUuid());
|
||||
|
||||
if (index == -1) {
|
||||
Log.w(TAG, "Deleted member on change not found in group");
|
||||
continue;
|
||||
}
|
||||
|
||||
DecryptedRequestingMember requestingMember = builder.getRequestingMembers(index);
|
||||
Member.Role role = approvedMember.getRole();
|
||||
|
||||
ensureKnownRole(role);
|
||||
|
||||
builder.removeRequestingMembers(index)
|
||||
.addMembers(DecryptedMember.newBuilder()
|
||||
.setUuid(approvedMember.getUuid())
|
||||
.setProfileKey(requestingMember.getProfileKey())
|
||||
.setRole(role));
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyInviteLinkPassword(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
if (!change.getNewInviteLinkPassword().isEmpty()) {
|
||||
builder.setInviteLinkPassword(change.getNewInviteLinkPassword());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,9 +485,22 @@ public final class DecryptedGroupUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private static void ensureKnownRole(Member.Role role) throws NotAbleToApplyGroupV2ChangeException {
|
||||
if (role != Member.Role.ADMINISTRATOR && role != Member.Role.DEFAULT) {
|
||||
throw new NotAbleToApplyGroupV2ChangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private static int indexOfUuid(List<DecryptedMember> memberList, ByteString uuid) {
|
||||
for (int i = 0; i < memberList.size(); i++) {
|
||||
if(uuid.equals(memberList.get(i).getUuid())) return i;
|
||||
if (uuid.equals(memberList.get(i).getUuid())) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int indexOfUuidInRequestingList(List<DecryptedRequestingMember> memberList, ByteString uuid) {
|
||||
for (int i = 0; i < memberList.size(); i++) {
|
||||
if (uuid.equals(memberList.get(i).getUuid())) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
@ -437,17 +517,22 @@ public final class DecryptedGroupUtil {
|
|||
}
|
||||
|
||||
public static boolean changeIsEmptyExceptForProfileKeyChanges(DecryptedGroupChange change) {
|
||||
return change.getNewMembersCount() == 0 && // field 3
|
||||
change.getDeleteMembersCount() == 0 && // field 4
|
||||
change.getModifyMemberRolesCount() == 0 && // field 5
|
||||
change.getNewPendingMembersCount() == 0 && // field 7
|
||||
change.getDeletePendingMembersCount() == 0 && // field 8
|
||||
change.getPromotePendingMembersCount() == 0 && // field 9
|
||||
!change.hasNewTitle() && // field 10
|
||||
!change.hasNewAvatar() && // field 11
|
||||
!change.hasNewTimer() && // field 12
|
||||
isSet(change.getNewAttributeAccess()) && // field 13
|
||||
isSet(change.getNewMemberAccess()); // field 14
|
||||
return change.getNewMembersCount() == 0 && // field 3
|
||||
change.getDeleteMembersCount() == 0 && // field 4
|
||||
change.getModifyMemberRolesCount() == 0 && // field 5
|
||||
change.getNewPendingMembersCount() == 0 && // field 7
|
||||
change.getDeletePendingMembersCount() == 0 && // field 8
|
||||
change.getPromotePendingMembersCount() == 0 && // field 9
|
||||
!change.hasNewTitle() && // field 10
|
||||
!change.hasNewAvatar() && // field 11
|
||||
!change.hasNewTimer() && // field 12
|
||||
isSet(change.getNewAttributeAccess()) && // field 13
|
||||
isSet(change.getNewMemberAccess()) && // field 14
|
||||
isSet(change.getNewInviteLinkAccess()) && // field 15
|
||||
change.getNewRequestingMembersCount() == 0 && // field 16
|
||||
change.getDeleteRequestingMembersCount() == 0 && // field 17
|
||||
change.getPromoteRequestingMembersCount() == 0 && // field 18
|
||||
change.getNewInviteLinkPassword().size() == 0; // field 19
|
||||
}
|
||||
|
||||
static boolean isSet(AccessControl.AccessRequired newAttributeAccess) {
|
||||
|
|
|
@ -2,17 +2,19 @@ package org.whispersystems.signalservice.api.groupsv2;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -51,16 +53,24 @@ public final class GroupChangeReconstruct {
|
|||
|
||||
Set<ByteString> pendingMembersListA = pendingMembersToSetOfUuids(fromState.getPendingMembersList());
|
||||
Set<ByteString> pendingMembersListB = pendingMembersToSetOfUuids(toState.getPendingMembersList());
|
||||
|
||||
Set<ByteString> requestingMembersListA = requestingMembersToSetOfUuids(fromState.getRequestingMembersList());
|
||||
Set<ByteString> requestingMembersListB = requestingMembersToSetOfUuids(toState.getRequestingMembersList());
|
||||
|
||||
Set<ByteString> removedPendingMemberUuids = subtract(pendingMembersListA, pendingMembersListB);
|
||||
Set<ByteString> newPendingMemberUuids = subtract(pendingMembersListB, pendingMembersListA);
|
||||
Set<ByteString> removedMemberUuids = subtract(fromStateMemberUuids, toStateMemberUuids);
|
||||
Set<ByteString> newMemberUuids = subtract(toStateMemberUuids, fromStateMemberUuids);
|
||||
Set<ByteString> removedPendingMemberUuids = subtract(pendingMembersListA, pendingMembersListB);
|
||||
Set<ByteString> removedRequestingMemberUuids = subtract(requestingMembersListA, requestingMembersListB);
|
||||
Set<ByteString> newPendingMemberUuids = subtract(pendingMembersListB, pendingMembersListA);
|
||||
Set<ByteString> newRequestingMemberUuids = subtract(requestingMembersListB, requestingMembersListA);
|
||||
Set<ByteString> removedMemberUuids = subtract(fromStateMemberUuids, toStateMemberUuids);
|
||||
Set<ByteString> newMemberUuids = subtract(toStateMemberUuids, fromStateMemberUuids);
|
||||
|
||||
Set<ByteString> addedByInvitationUuids = intersect(newMemberUuids, removedPendingMemberUuids);
|
||||
Set<DecryptedMember> addedMembersByInvitation = intersectByUUID(toState.getMembersList(), addedByInvitationUuids);
|
||||
Set<DecryptedMember> addedMembers = intersectByUUID(toState.getMembersList(), subtract(newMemberUuids, addedByInvitationUuids));
|
||||
Set<DecryptedPendingMember> uninvitedMembers = intersectPendingByUUID(fromState.getPendingMembersList(), subtract(removedPendingMemberUuids, addedByInvitationUuids));
|
||||
Set<ByteString> addedByInvitationUuids = intersect(newMemberUuids, removedPendingMemberUuids);
|
||||
Set<ByteString> addedByRequestApprovalUuids = intersect(newMemberUuids, removedRequestingMemberUuids);
|
||||
Set<DecryptedMember> addedMembersByInvitation = intersectByUUID(toState.getMembersList(), addedByInvitationUuids);
|
||||
Set<DecryptedMember> addedMembersByRequestApproval = intersectByUUID(toState.getMembersList(), addedByRequestApprovalUuids);
|
||||
Set<DecryptedMember> addedMembers = intersectByUUID(toState.getMembersList(), subtract(newMemberUuids, addedByInvitationUuids, addedByRequestApprovalUuids));
|
||||
Set<DecryptedPendingMember> uninvitedMembers = intersectPendingByUUID(fromState.getPendingMembersList(), subtract(removedPendingMemberUuids, addedByInvitationUuids));
|
||||
Set<DecryptedRequestingMember> rejectedRequestMembers = intersectRequestingByUUID(fromState.getRequestingMembersList(), subtract(removedRequestingMemberUuids, addedByRequestApprovalUuids));
|
||||
|
||||
for (DecryptedMember member : intersectByUUID(fromState.getMembersList(), removedMemberUuids)) {
|
||||
builder.addDeleteMembers(member.getUuid());
|
||||
|
@ -101,11 +111,33 @@ public final class GroupChangeReconstruct {
|
|||
}
|
||||
}
|
||||
|
||||
if (!fromState.getAccessControl().getAddFromInviteLink().equals(toState.getAccessControl().getAddFromInviteLink())) {
|
||||
builder.setNewInviteLinkAccess(toState.getAccessControl().getAddFromInviteLink());
|
||||
}
|
||||
|
||||
for (DecryptedRequestingMember requestingMember : intersectRequestingByUUID(toState.getRequestingMembersList(), newRequestingMemberUuids)) {
|
||||
builder.addNewRequestingMembers(requestingMember);
|
||||
}
|
||||
|
||||
for (DecryptedRequestingMember requestingMember : rejectedRequestMembers) {
|
||||
builder.addDeleteRequestingMembers(requestingMember.getUuid());
|
||||
}
|
||||
|
||||
for (DecryptedMember member : addedMembersByRequestApproval) {
|
||||
builder.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setUuid(member.getUuid())
|
||||
.setRole(member.getRole()));
|
||||
}
|
||||
|
||||
if (!fromState.getInviteLinkPassword().equals(toState.getInviteLinkPassword())) {
|
||||
builder.setNewInviteLinkPassword(toState.getInviteLinkPassword());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static Map<ByteString, DecryptedMember> uuidMap(List<DecryptedMember> membersList) {
|
||||
HashMap<ByteString, DecryptedMember> map = new HashMap<>(membersList.size());
|
||||
Map<ByteString, DecryptedMember> map = new LinkedHashMap<>(membersList.size());
|
||||
for (DecryptedMember member : membersList) {
|
||||
map.put(member.getUuid(), member);
|
||||
}
|
||||
|
@ -113,7 +145,7 @@ public final class GroupChangeReconstruct {
|
|||
}
|
||||
|
||||
private static Set<DecryptedMember> intersectByUUID(Collection<DecryptedMember> members, Set<ByteString> uuids) {
|
||||
Set<DecryptedMember> result = new HashSet<>(members.size());
|
||||
Set<DecryptedMember> result = new LinkedHashSet<>(members.size());
|
||||
for (DecryptedMember member : members) {
|
||||
if (uuids.contains(member.getUuid()))
|
||||
result.add(member);
|
||||
|
@ -122,24 +154,41 @@ public final class GroupChangeReconstruct {
|
|||
}
|
||||
|
||||
private static Set<DecryptedPendingMember> intersectPendingByUUID(Collection<DecryptedPendingMember> members, Set<ByteString> uuids) {
|
||||
Set<DecryptedPendingMember> result = new HashSet<>(members.size());
|
||||
Set<DecryptedPendingMember> result = new LinkedHashSet<>(members.size());
|
||||
for (DecryptedPendingMember member : members) {
|
||||
if (uuids.contains(member.getUuid()))
|
||||
result.add(member);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<DecryptedRequestingMember> intersectRequestingByUUID(Collection<DecryptedRequestingMember> members, Set<ByteString> uuids) {
|
||||
Set<DecryptedRequestingMember> result = new LinkedHashSet<>(members.size());
|
||||
for (DecryptedRequestingMember member : members) {
|
||||
if (uuids.contains(member.getUuid()))
|
||||
result.add(member);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<ByteString> pendingMembersToSetOfUuids(Collection<DecryptedPendingMember> pendingMembers) {
|
||||
HashSet<ByteString> uuids = new HashSet<>(pendingMembers.size());
|
||||
Set<ByteString> uuids = new LinkedHashSet<>(pendingMembers.size());
|
||||
for (DecryptedPendingMember pendingMember : pendingMembers) {
|
||||
uuids.add(pendingMember.getUuid());
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
private static Set<ByteString> requestingMembersToSetOfUuids(Collection<DecryptedRequestingMember> requestingMembers) {
|
||||
Set<ByteString> uuids = new LinkedHashSet<>(requestingMembers.size());
|
||||
for (DecryptedRequestingMember requestingMember : requestingMembers) {
|
||||
uuids.add(requestingMember.getUuid());
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
private static Set<ByteString> membersToSetOfUuids(Collection<DecryptedMember> members) {
|
||||
HashSet<ByteString> uuids = new HashSet<>(members.size());
|
||||
Set<ByteString> uuids = new LinkedHashSet<>(members.size());
|
||||
for (DecryptedMember member : members) {
|
||||
uuids.add(member.getUuid());
|
||||
}
|
||||
|
@ -147,13 +196,20 @@ public final class GroupChangeReconstruct {
|
|||
}
|
||||
|
||||
private static <T> Set<T> subtract(Collection<T> a, Collection<T> b) {
|
||||
Set<T> result = new HashSet<>(a);
|
||||
Set<T> result = new LinkedHashSet<>(a);
|
||||
result.removeAll(b);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T> Set<T> subtract(Collection<T> a, Collection<T> b, Collection<T> c) {
|
||||
Set<T> result = new LinkedHashSet<>(a);
|
||||
result.removeAll(b);
|
||||
result.removeAll(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T> Set<T> intersect(Collection<T> a, Collection<T> b) {
|
||||
Set<T> result = new HashSet<>(a);
|
||||
Set<T> result = new LinkedHashSet<>(a);
|
||||
result.retainAll(b);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ package org.whispersystems.signalservice.api.groupsv2;
|
|||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -18,27 +20,27 @@ public final class GroupChangeUtil {
|
|||
private GroupChangeUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum field we know about here.
|
||||
*/
|
||||
static final int CHANGE_ACTION_MAX_FIELD = 14;
|
||||
|
||||
/**
|
||||
* True iff there are no change actions.
|
||||
*/
|
||||
public static boolean changeIsEmpty(GroupChange.Actions change) {
|
||||
return change.getAddMembersCount() == 0 && // field 3
|
||||
change.getDeleteMembersCount() == 0 && // field 4
|
||||
change.getModifyMemberRolesCount() == 0 && // field 5
|
||||
change.getModifyMemberProfileKeysCount() == 0 && // field 6
|
||||
change.getAddPendingMembersCount() == 0 && // field 7
|
||||
change.getDeletePendingMembersCount() == 0 && // field 8
|
||||
change.getPromotePendingMembersCount() == 0 && // field 9
|
||||
!change.hasModifyTitle() && // field 10
|
||||
!change.hasModifyAvatar() && // field 11
|
||||
!change.hasModifyDisappearingMessagesTimer() && // field 12
|
||||
!change.hasModifyAttributesAccess() && // field 13
|
||||
!change.hasModifyMemberAccess(); // field 14
|
||||
return change.getAddMembersCount() == 0 && // field 3
|
||||
change.getDeleteMembersCount() == 0 && // field 4
|
||||
change.getModifyMemberRolesCount() == 0 && // field 5
|
||||
change.getModifyMemberProfileKeysCount() == 0 && // field 6
|
||||
change.getAddPendingMembersCount() == 0 && // field 7
|
||||
change.getDeletePendingMembersCount() == 0 && // field 8
|
||||
change.getPromotePendingMembersCount() == 0 && // field 9
|
||||
!change.hasModifyTitle() && // field 10
|
||||
!change.hasModifyAvatar() && // field 11
|
||||
!change.hasModifyDisappearingMessagesTimer() && // field 12
|
||||
!change.hasModifyAttributesAccess() && // field 13
|
||||
!change.hasModifyMemberAccess() && // field 14
|
||||
!change.hasModifyAddFromInviteLinkAccess() && // field 15
|
||||
change.getAddRequestingMembersCount() == 0 && // field 16
|
||||
change.getDeleteRequestingMembersCount() == 0 && // field 17
|
||||
change.getPromoteRequestingMembersCount() == 0 && // field 18
|
||||
!change.hasModifyInviteLinkPassword(); // field 19
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,9 +64,10 @@ public final class GroupChangeUtil {
|
|||
DecryptedGroupChange conflictingChange,
|
||||
GroupChange.Actions encryptedChange)
|
||||
{
|
||||
GroupChange.Actions.Builder result = GroupChange.Actions.newBuilder(encryptedChange);
|
||||
HashMap<ByteString, DecryptedMember> fullMembersByUuid = new HashMap<>(groupState.getMembersCount());
|
||||
HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid = new HashMap<>(groupState.getPendingMembersCount());
|
||||
GroupChange.Actions.Builder result = GroupChange.Actions.newBuilder(encryptedChange);
|
||||
HashMap<ByteString, DecryptedMember> fullMembersByUuid = new HashMap<>(groupState.getMembersCount());
|
||||
HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid = new HashMap<>(groupState.getPendingMembersCount());
|
||||
HashMap<ByteString, DecryptedRequestingMember> requestingMembersByUuid = new HashMap<>(groupState.getMembersCount());
|
||||
|
||||
for (DecryptedMember member : groupState.getMembersList()) {
|
||||
fullMembersByUuid.put(member.getUuid(), member);
|
||||
|
@ -74,6 +77,10 @@ public final class GroupChangeUtil {
|
|||
pendingMembersByUuid.put(member.getUuid(), member);
|
||||
}
|
||||
|
||||
for (DecryptedRequestingMember member : groupState.getRequestingMembersList()) {
|
||||
requestingMembersByUuid.put(member.getUuid(), member);
|
||||
}
|
||||
|
||||
resolveField3AddMembers (conflictingChange, result, fullMembersByUuid, pendingMembersByUuid);
|
||||
resolveField4DeleteMembers (conflictingChange, result, fullMembersByUuid);
|
||||
resolveField5ModifyMemberRoles (conflictingChange, result, fullMembersByUuid);
|
||||
|
@ -86,6 +93,10 @@ public final class GroupChangeUtil {
|
|||
resolveField12modifyDisappearingMessagesTimer(groupState, conflictingChange, result);
|
||||
resolveField13modifyAttributesAccess (groupState, conflictingChange, result);
|
||||
resolveField14modifyAttributesAccess (groupState, conflictingChange, result);
|
||||
resolveField15modifyAddFromInviteLinkAccess (groupState, conflictingChange, result);
|
||||
resolveField16AddRequestingMembers (conflictingChange, result, fullMembersByUuid, pendingMembersByUuid);
|
||||
resolveField17DeleteMembers (conflictingChange, result, requestingMembersByUuid);
|
||||
resolveField18PromoteRequestingMembers (conflictingChange, result, requestingMembersByUuid);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -209,4 +220,56 @@ public final class GroupChangeUtil {
|
|||
result.clearModifyMemberAccess();
|
||||
}
|
||||
}
|
||||
|
||||
private static void resolveField15modifyAddFromInviteLinkAccess(DecryptedGroup groupState, DecryptedGroupChange conflictingChange, GroupChange.Actions.Builder result) {
|
||||
if (conflictingChange.getNewInviteLinkAccess() == groupState.getAccessControl().getAddFromInviteLink()) {
|
||||
result.clearModifyAddFromInviteLinkAccess();
|
||||
}
|
||||
}
|
||||
|
||||
private static void resolveField16AddRequestingMembers(DecryptedGroupChange conflictingChange, GroupChange.Actions.Builder result, HashMap<ByteString, DecryptedMember> fullMembersByUuid, HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid) {
|
||||
List<DecryptedRequestingMember> newMembersList = conflictingChange.getNewRequestingMembersList();
|
||||
|
||||
for (int i = newMembersList.size() - 1; i >= 0; i--) {
|
||||
DecryptedRequestingMember member = newMembersList.get(i);
|
||||
|
||||
if (fullMembersByUuid.containsKey(member.getUuid())) {
|
||||
result.removeAddRequestingMembers(i);
|
||||
} else if (pendingMembersByUuid.containsKey(member.getUuid())) {
|
||||
GroupChange.Actions.AddRequestingMemberAction addMemberAction = result.getAddRequestingMembersList().get(i);
|
||||
result.removeAddRequestingMembers(i);
|
||||
result.addPromotePendingMembers(0, GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(addMemberAction.getAdded().getPresentation()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void resolveField17DeleteMembers(DecryptedGroupChange conflictingChange,
|
||||
GroupChange.Actions.Builder result,
|
||||
HashMap<ByteString, DecryptedRequestingMember> requestingMembers)
|
||||
{
|
||||
List<ByteString> deletedMembersList = conflictingChange.getDeleteRequestingMembersList();
|
||||
|
||||
for (int i = deletedMembersList.size() - 1; i >= 0; i--) {
|
||||
ByteString member = deletedMembersList.get(i);
|
||||
|
||||
if (!requestingMembers.containsKey(member)) {
|
||||
result.removeDeleteRequestingMembers(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void resolveField18PromoteRequestingMembers(DecryptedGroupChange conflictingChange,
|
||||
GroupChange.Actions.Builder result,
|
||||
HashMap<ByteString, DecryptedRequestingMember> requestingMembersByUuid)
|
||||
{
|
||||
List<DecryptedApproveMember> promoteRequestingMembersList = conflictingChange.getPromoteRequestingMembersList();
|
||||
|
||||
for (int i = promoteRequestingMembersList.size() - 1; i >= 0; i--) {
|
||||
DecryptedApproveMember member = promoteRequestingMembersList.get(i);
|
||||
|
||||
if (!requestingMembersByUuid.containsKey(member.getUuid())) {
|
||||
result.removePromoteRequestingMembers(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
/**
|
||||
* Thrown when a group link:
|
||||
* - has an out of date password, or;
|
||||
* - is currently not shared, or;
|
||||
* - the master key does not match a group on the server
|
||||
*/
|
||||
public final class GroupLinkNotActiveException extends Exception {
|
||||
GroupLinkNotActiveException() {
|
||||
}
|
||||
}
|
|
@ -7,8 +7,10 @@ import org.signal.storageservice.protos.groups.Group;
|
|||
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.auth.AuthCredential;
|
||||
|
@ -19,6 +21,7 @@ import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
|||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -110,6 +113,21 @@ public final class GroupsV2Api {
|
|||
return result;
|
||||
}
|
||||
|
||||
public DecryptedGroupJoinInfo getGroupJoinInfo(GroupSecretParams groupSecretParams,
|
||||
byte[] password,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException, GroupLinkNotActiveException
|
||||
{
|
||||
try {
|
||||
GroupJoinInfo joinInfo = socket.getGroupJoinInfo(password, authorization);
|
||||
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
|
||||
|
||||
return groupOperations.decryptGroupJoinInfo(joinInfo);
|
||||
} catch (ForbiddenException e) {
|
||||
throw new GroupLinkNotActiveException();
|
||||
}
|
||||
}
|
||||
|
||||
public String uploadAvatar(byte[] avatar,
|
||||
GroupSecretParams groupSecretParams,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
|
|
|
@ -7,14 +7,19 @@ import org.signal.storageservice.protos.groups.AccessControl;
|
|||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.PendingMember;
|
||||
import org.signal.storageservice.protos.groups.RequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
|
@ -54,7 +59,7 @@ public final class GroupsV2Operations {
|
|||
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
|
||||
|
||||
/** Highest change epoch this class knows now to decrypt */
|
||||
public static final int HIGHEST_KNOWN_EPOCH = 0;
|
||||
public static final int HIGHEST_KNOWN_EPOCH = 1;
|
||||
|
||||
private final ServerPublicParams serverPublicParams;
|
||||
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||
|
@ -137,8 +142,9 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
|
||||
public GroupChange.Actions.Builder createModifyGroupTitle(final String title) {
|
||||
return GroupChange.Actions.newBuilder().setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder()
|
||||
.setTitle(encryptTitle(title)));
|
||||
return GroupChange.Actions.newBuilder().setModifyTitle(GroupChange.Actions.ModifyTitleAction
|
||||
.newBuilder()
|
||||
.setTitle(encryptTitle(title)));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, UUID selfUuid) {
|
||||
|
@ -151,24 +157,75 @@ public final class GroupsV2Operations {
|
|||
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
|
||||
|
||||
if (profileKeyCredential != null) {
|
||||
actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder()
|
||||
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
|
||||
actions.addAddMembers(GroupChange.Actions.AddMemberAction
|
||||
.newBuilder()
|
||||
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
|
||||
} else {
|
||||
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder()
|
||||
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)
|
||||
.setAddedByUserId(encryptUuid(selfUuid))));
|
||||
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction
|
||||
.newBuilder()
|
||||
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)
|
||||
.setAddedByUserId(encryptUuid(selfUuid))));
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createGroupJoinRequest(ProfileKeyCredential profileKeyCredential) {
|
||||
GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||
|
||||
actions.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction
|
||||
.newBuilder()
|
||||
.setAdded(groupOperations.requestingMember(profileKeyCredential)));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createGroupJoinDirect(ProfileKeyCredential profileKeyCredential) {
|
||||
GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||
|
||||
actions.addAddMembers(GroupChange.Actions.AddMemberAction
|
||||
.newBuilder()
|
||||
.setAdded(groupOperations.member(profileKeyCredential, Member.Role.DEFAULT))
|
||||
.setJoinFromInviteLink(true));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createRefuseGroupJoinRequest(Set<UUID> requestsToRemove) {
|
||||
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||
|
||||
for (UUID uuid : requestsToRemove) {
|
||||
actions.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction
|
||||
.newBuilder()
|
||||
.setDeletedUserId(encryptUuid(uuid)));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createApproveGroupJoinRequest(Set<UUID> requestsToApprove) {
|
||||
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||
|
||||
for (UUID uuid : requestsToApprove) {
|
||||
actions.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction
|
||||
.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUserId(encryptUuid(uuid)));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createRemoveMembersChange(final Set<UUID> membersToRemove) {
|
||||
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||
|
||||
for (UUID remove: membersToRemove) {
|
||||
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder()
|
||||
.setDeletedUserId(encryptUuid(remove)));
|
||||
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction
|
||||
.newBuilder()
|
||||
.setDeletedUserId(encryptUuid(remove)));
|
||||
}
|
||||
|
||||
return actions;
|
||||
|
@ -178,9 +235,10 @@ public final class GroupsV2Operations {
|
|||
GroupChange.Actions.Builder actions = createRemoveMembersChange(Collections.singleton(self));
|
||||
|
||||
for (UUID member : membersToMakeAdmin) {
|
||||
actions.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder()
|
||||
.setUserId(encryptUuid(member))
|
||||
.setRole(Member.Role.ADMINISTRATOR));
|
||||
actions.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction
|
||||
.newBuilder()
|
||||
.setUserId(encryptUuid(member))
|
||||
.setRole(Member.Role.ADMINISTRATOR));
|
||||
}
|
||||
|
||||
return actions;
|
||||
|
@ -189,7 +247,8 @@ public final class GroupsV2Operations {
|
|||
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction
|
||||
.newBuilder()
|
||||
.setTimer(encryptTimer(timerDurationSeconds)));
|
||||
}
|
||||
|
||||
|
@ -198,7 +257,8 @@ public final class GroupsV2Operations {
|
|||
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction
|
||||
.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize())));
|
||||
}
|
||||
|
||||
|
@ -207,8 +267,9 @@ public final class GroupsV2Operations {
|
|||
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize())));
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction
|
||||
.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize())));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createRemoveInvitationChange(final Set<UuidCiphertext> uuidCipherTextsFromInvitesToRemove) {
|
||||
|
@ -216,37 +277,90 @@ public final class GroupsV2Operations {
|
|||
.newBuilder();
|
||||
|
||||
for (UuidCiphertext uuidCipherText: uuidCipherTextsFromInvitesToRemove) {
|
||||
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder()
|
||||
.setDeletedUserId(ByteString.copyFrom(uuidCipherText.serialize())));
|
||||
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction
|
||||
.newBuilder()
|
||||
.setDeletedUserId(ByteString.copyFrom(uuidCipherText.serialize())));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createModifyGroupLinkPasswordChange(byte[] groupLinkPassword) {
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction
|
||||
.newBuilder()
|
||||
.setInviteLinkPassword(ByteString.copyFrom(groupLinkPassword)));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createModifyGroupLinkPasswordAndRightsChange(byte[] groupLinkPassword, AccessControl.AccessRequired newRights) {
|
||||
GroupChange.Actions.Builder change = createModifyGroupLinkPasswordChange(groupLinkPassword);
|
||||
|
||||
return change.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction
|
||||
.newBuilder()
|
||||
.setAddFromInviteLinkAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeJoinByLinkRights(AccessControl.AccessRequired newRights) {
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction
|
||||
.newBuilder()
|
||||
.setAddFromInviteLinkAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeMembershipRights(AccessControl.AccessRequired newRights) {
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction
|
||||
.newBuilder()
|
||||
.setMembersAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeAttributesRights(AccessControl.AccessRequired newRights) {
|
||||
return GroupChange.Actions
|
||||
.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction
|
||||
.newBuilder()
|
||||
.setAttributesAccess(newRights));
|
||||
}
|
||||
|
||||
private Member.Builder member(ProfileKeyCredential credential, Member.Role role) {
|
||||
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
|
||||
|
||||
return Member.newBuilder().setRole(role)
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize()));
|
||||
return Member.newBuilder()
|
||||
.setRole(role)
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize()));
|
||||
}
|
||||
|
||||
private RequestingMember.Builder requestingMember(ProfileKeyCredential credential) {
|
||||
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
|
||||
|
||||
return RequestingMember.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize()));
|
||||
}
|
||||
|
||||
public PendingMember.Builder invitee(UUID uuid, Member.Role role) {
|
||||
UuidCiphertext uuidCiphertext = clientZkGroupCipher.encryptUuid(uuid);
|
||||
|
||||
Member member = Member.newBuilder().setRole(role)
|
||||
.setUserId(ByteString.copyFrom(uuidCiphertext.serialize()))
|
||||
.build();
|
||||
Member member = Member.newBuilder()
|
||||
.setRole(role)
|
||||
.setUserId(ByteString.copyFrom(uuidCiphertext.serialize()))
|
||||
.build();
|
||||
|
||||
return PendingMember.newBuilder().setMember(member);
|
||||
return PendingMember.newBuilder()
|
||||
.setMember(member);
|
||||
}
|
||||
|
||||
public DecryptedGroup decryptGroup(Group group)
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
List<Member> membersList = group.getMembersList();
|
||||
List<PendingMember> pendingMembersList = group.getPendingMembersList();
|
||||
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
|
||||
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
|
||||
List<Member> membersList = group.getMembersList();
|
||||
List<PendingMember> pendingMembersList = group.getPendingMembersList();
|
||||
List<RequestingMember> requestingMembersList = group.getRequestingMembersList();
|
||||
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
|
||||
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
|
||||
List<DecryptedRequestingMember> decryptedRequestingMembers = new ArrayList<>(requestingMembersList.size());
|
||||
|
||||
for (Member member : membersList) {
|
||||
try {
|
||||
|
@ -260,6 +374,10 @@ public final class GroupsV2Operations {
|
|||
decryptedPendingMembers.add(decryptMember(member));
|
||||
}
|
||||
|
||||
for (RequestingMember member : requestingMembersList) {
|
||||
decryptedRequestingMembers.add(decryptRequestingMember(member));
|
||||
}
|
||||
|
||||
return DecryptedGroup.newBuilder()
|
||||
.setTitle(decryptTitle(group.getTitle()))
|
||||
.setAvatar(group.getAvatar())
|
||||
|
@ -267,7 +385,9 @@ public final class GroupsV2Operations {
|
|||
.setRevision(group.getRevision())
|
||||
.addAllMembers(decryptedMembers)
|
||||
.addAllPendingMembers(decryptedPendingMembers)
|
||||
.addAllRequestingMembers(decryptedRequestingMembers)
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer())))
|
||||
.setInviteLinkPassword(group.getInviteLinkPassword())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -419,9 +539,44 @@ public final class GroupsV2Operations {
|
|||
builder.setNewMemberAccess(actions.getModifyMemberAccess().getMembersAccess());
|
||||
}
|
||||
|
||||
// Field 15
|
||||
if (actions.hasModifyAddFromInviteLinkAccess()) {
|
||||
builder.setNewInviteLinkAccess(actions.getModifyAddFromInviteLinkAccess().getAddFromInviteLinkAccess());
|
||||
}
|
||||
|
||||
// Field 16
|
||||
for (GroupChange.Actions.AddRequestingMemberAction request : actions.getAddRequestingMembersList()) {
|
||||
builder.addNewRequestingMembers(decryptRequestingMember(request.getAdded()));
|
||||
}
|
||||
|
||||
// Field 17
|
||||
for (GroupChange.Actions.DeleteRequestingMemberAction delete : actions.getDeleteRequestingMembersList()) {
|
||||
builder.addDeleteRequestingMembers(decryptUuidToByteString(delete.getDeletedUserId()));
|
||||
}
|
||||
|
||||
// Field 18
|
||||
for (GroupChange.Actions.PromoteRequestingMemberAction promote : actions.getPromoteRequestingMembersList()) {
|
||||
builder.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder().setRole(promote.getRole()).setUuid(decryptUuidToByteString(promote.getUserId())));
|
||||
}
|
||||
|
||||
// Field 19
|
||||
if (actions.hasModifyInviteLinkPassword()) {
|
||||
builder.setNewInviteLinkPassword(actions.getModifyInviteLinkPassword().getInviteLinkPassword());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public DecryptedGroupJoinInfo decryptGroupJoinInfo(GroupJoinInfo joinInfo) {
|
||||
return DecryptedGroupJoinInfo.newBuilder()
|
||||
.setTitle(decryptTitle(joinInfo.getTitle()))
|
||||
.setAvatar(joinInfo.getAvatar())
|
||||
.setMemberCount(joinInfo.getMemberCount())
|
||||
.setAddFromInviteLink(joinInfo.getAddFromInviteLink())
|
||||
.setRevision(joinInfo.getRevision())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DecryptedMember.Builder decryptMember(Member member)
|
||||
throws InvalidGroupStateException, VerificationFailedException, InvalidInputException
|
||||
{
|
||||
|
@ -453,14 +608,50 @@ public final class GroupsV2Operations {
|
|||
UUID uuid = decryptUuidOrUnknown(userIdCipherText);
|
||||
UUID addedBy = decryptUuid(member.getAddedByUserId());
|
||||
|
||||
Member.Role role = member.getMember().getRole();
|
||||
|
||||
if (role != Member.Role.ADMINISTRATOR && role != Member.Role.DEFAULT) {
|
||||
role = Member.Role.DEFAULT;
|
||||
}
|
||||
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(userIdCipherText)
|
||||
.setAddedByUuid(ByteString.copyFrom(UUIDUtil.serialize(addedBy)))
|
||||
.setRole(member.getMember().getRole())
|
||||
.setRole(role)
|
||||
.setTimestamp(member.getTimestamp())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DecryptedRequestingMember decryptRequestingMember(RequestingMember member)
|
||||
throws InvalidGroupStateException, VerificationFailedException
|
||||
{
|
||||
if (member.getPresentation().isEmpty()) {
|
||||
UUID uuid = decryptUuid(member.getUserId());
|
||||
|
||||
return DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
|
||||
.setTimestamp(member.getTimestamp())
|
||||
.build();
|
||||
} else {
|
||||
ProfileKeyCredentialPresentation profileKeyCredentialPresentation;
|
||||
try {
|
||||
profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(member.getPresentation().toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new InvalidGroupStateException(e);
|
||||
}
|
||||
|
||||
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
|
||||
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
|
||||
|
||||
return DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileKey decryptProfileKey(ByteString profileKey, UUID uuid) throws VerificationFailedException, InvalidGroupStateException {
|
||||
try {
|
||||
ProfileKeyCiphertext profileKeyCiphertext = new ProfileKeyCiphertext(profileKey.toByteArray());
|
||||
|
@ -501,7 +692,7 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
}
|
||||
|
||||
private ByteString encryptTitle(String title) {
|
||||
ByteString encryptTitle(String title) {
|
||||
try {
|
||||
GroupAttributeBlob blob = GroupAttributeBlob.newBuilder().setTitle(title).build();
|
||||
|
||||
|
@ -544,7 +735,7 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
}
|
||||
|
||||
private ByteString encryptTimer(int timerDurationSeconds) {
|
||||
ByteString encryptTimer(int timerDurationSeconds) {
|
||||
try {
|
||||
GroupAttributeBlob timer = GroupAttributeBlob.newBuilder()
|
||||
.setDisappearingMessagesDuration(timerDurationSeconds)
|
||||
|
@ -584,18 +775,6 @@ public final class GroupsV2Operations {
|
|||
return GroupChange.Actions.parseFrom(groupChange.getActions());
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeMembershipRights(AccessControl.AccessRequired newRights) {
|
||||
return GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder()
|
||||
.setMembersAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeAttributesRights(AccessControl.AccessRequired newRights) {
|
||||
return GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder()
|
||||
.setAttributesAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeMemberRole(UUID uuid, Member.Role role) {
|
||||
return GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder()
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
|
|||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
|
@ -72,6 +73,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
|
|||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
@ -93,6 +95,7 @@ import org.whispersystems.signalservice.internal.util.concurrent.FutureTransform
|
|||
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.util.Base64;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -102,7 +105,6 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -194,8 +196,10 @@ public class PushServiceSocket {
|
|||
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
|
||||
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s";
|
||||
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
|
||||
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
|
||||
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
|
||||
|
@ -1910,6 +1914,9 @@ public class PushServiceSocket {
|
|||
private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = responseCode -> {
|
||||
if (responseCode == 400) throw new GroupPatchNotAcceptedException();
|
||||
};
|
||||
private static final ResponseCodeHandler GROUPS_V2_GET_JOIN_INFO_HANDLER = responseCode -> {
|
||||
if (responseCode == 403) throw new ForbiddenException();
|
||||
};
|
||||
|
||||
public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
|
@ -1969,6 +1976,18 @@ public class PushServiceSocket {
|
|||
return GroupChanges.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
public GroupJoinInfo getGroupJoinInfo(byte[] groupLinkPassword, GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
ResponseBody response = makeStorageRequest(authorization.toString(),
|
||||
String.format(GROUPSV2_GROUP_JOIN, Base64UrlSafe.encodeBytesWithoutPadding(groupLinkPassword)),
|
||||
"GET",
|
||||
null,
|
||||
GROUPS_V2_GET_JOIN_INFO_HANDLER);
|
||||
|
||||
return GroupJoinInfo.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
private final class ResumeInfo {
|
||||
private final String contentRange;
|
||||
private final long contentStart;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
public final class ForbiddenException extends NonSuccessfulResponseCodeException {
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.util.Base64;
|
||||
package org.whispersystems.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -11,11 +7,11 @@ public final class Base64UrlSafe {
|
|||
private Base64UrlSafe() {
|
||||
}
|
||||
|
||||
public static @NonNull byte[] decode(@NonNull String s) throws IOException {
|
||||
public static byte[] decode(String s) throws IOException {
|
||||
return Base64.decode(s, Base64.URL_SAFE);
|
||||
}
|
||||
|
||||
public static @NonNull byte[] decodePaddingAgnostic(@NonNull String s) throws IOException {
|
||||
public static byte[] decodePaddingAgnostic(String s) throws IOException {
|
||||
switch (s.length() % 4) {
|
||||
case 1:
|
||||
case 3: s = s + "="; break;
|
||||
|
@ -24,7 +20,7 @@ public final class Base64UrlSafe {
|
|||
return decode(s);
|
||||
}
|
||||
|
||||
public static @NonNull String encodeBytes(@NonNull byte[] source) {
|
||||
public static String encodeBytes(byte[] source) {
|
||||
try {
|
||||
return Base64.encodeBytes(source, Base64.URL_SAFE);
|
||||
} catch (IOException e) {
|
||||
|
@ -32,7 +28,7 @@ public final class Base64UrlSafe {
|
|||
}
|
||||
}
|
||||
|
||||
public static @NonNull String encodeBytesWithoutPadding(@NonNull byte[] source) {
|
||||
public static String encodeBytesWithoutPadding(byte[] source) {
|
||||
return encodeBytes(source).replace("=", "");
|
||||
}
|
||||
}
|
|
@ -27,45 +27,63 @@ message DecryptedPendingMember {
|
|||
bytes uuidCipherText = 5;
|
||||
}
|
||||
|
||||
message DecryptedRequestingMember {
|
||||
bytes uuid = 1;
|
||||
bytes profileKey = 2;
|
||||
uint64 timestamp = 4;
|
||||
}
|
||||
|
||||
message DecryptedPendingMemberRemoval {
|
||||
bytes uuid = 1;
|
||||
bytes uuidCipherText = 2;
|
||||
bytes uuid = 1;
|
||||
bytes uuidCipherText = 2;
|
||||
}
|
||||
|
||||
message DecryptedApproveMember {
|
||||
bytes uuid = 1;
|
||||
Member.Role role = 2;
|
||||
}
|
||||
|
||||
message DecryptedModifyMemberRole {
|
||||
bytes uuid = 1;
|
||||
Member.Role role = 2;
|
||||
bytes uuid = 1;
|
||||
Member.Role role = 2;
|
||||
}
|
||||
|
||||
// Decrypted version of message Group
|
||||
// Keep field numbers in step
|
||||
message DecryptedGroup {
|
||||
string title = 2;
|
||||
string avatar = 3;
|
||||
DecryptedTimer disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 revision = 6;
|
||||
repeated DecryptedMember members = 7;
|
||||
repeated DecryptedPendingMember pendingMembers = 8;
|
||||
string title = 2;
|
||||
string avatar = 3;
|
||||
DecryptedTimer disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 revision = 6;
|
||||
repeated DecryptedMember members = 7;
|
||||
repeated DecryptedPendingMember pendingMembers = 8;
|
||||
repeated DecryptedRequestingMember requestingMembers = 9;
|
||||
bytes inviteLinkPassword = 10;
|
||||
}
|
||||
|
||||
// Decrypted version of message GroupChange.Actions
|
||||
// Keep field numbers in step
|
||||
message DecryptedGroupChange {
|
||||
bytes editor = 1;
|
||||
uint32 revision = 2;
|
||||
repeated DecryptedMember newMembers = 3;
|
||||
repeated bytes deleteMembers = 4;
|
||||
repeated DecryptedModifyMemberRole modifyMemberRoles = 5;
|
||||
repeated DecryptedMember modifiedProfileKeys = 6;
|
||||
repeated DecryptedPendingMember newPendingMembers = 7;
|
||||
repeated DecryptedPendingMemberRemoval deletePendingMembers = 8;
|
||||
repeated DecryptedMember promotePendingMembers = 9;
|
||||
DecryptedString newTitle = 10;
|
||||
DecryptedString newAvatar = 11;
|
||||
DecryptedTimer newTimer = 12;
|
||||
AccessControl.AccessRequired newAttributeAccess = 13;
|
||||
AccessControl.AccessRequired newMemberAccess = 14;
|
||||
bytes editor = 1;
|
||||
uint32 revision = 2;
|
||||
repeated DecryptedMember newMembers = 3;
|
||||
repeated bytes deleteMembers = 4;
|
||||
repeated DecryptedModifyMemberRole modifyMemberRoles = 5;
|
||||
repeated DecryptedMember modifiedProfileKeys = 6;
|
||||
repeated DecryptedPendingMember newPendingMembers = 7;
|
||||
repeated DecryptedPendingMemberRemoval deletePendingMembers = 8;
|
||||
repeated DecryptedMember promotePendingMembers = 9;
|
||||
DecryptedString newTitle = 10;
|
||||
DecryptedString newAvatar = 11;
|
||||
DecryptedTimer newTimer = 12;
|
||||
AccessControl.AccessRequired newAttributeAccess = 13;
|
||||
AccessControl.AccessRequired newMemberAccess = 14;
|
||||
AccessControl.AccessRequired newInviteLinkAccess = 15;
|
||||
repeated DecryptedRequestingMember newRequestingMembers = 16;
|
||||
repeated bytes deleteRequestingMembers = 17;
|
||||
repeated DecryptedApproveMember promoteRequestingMembers = 18;
|
||||
bytes newInviteLinkPassword = 19;
|
||||
}
|
||||
|
||||
message DecryptedString {
|
||||
|
@ -75,3 +93,11 @@ message DecryptedString {
|
|||
message DecryptedTimer {
|
||||
uint32 duration = 1;
|
||||
}
|
||||
|
||||
message DecryptedGroupJoinInfo {
|
||||
string title = 2;
|
||||
string avatar = 3;
|
||||
uint32 memberCount = 4;
|
||||
AccessControl.AccessRequired addFromInviteLink = 5;
|
||||
uint32 revision = 6;
|
||||
}
|
||||
|
|
|
@ -38,26 +38,38 @@ message PendingMember {
|
|||
uint64 timestamp = 3;
|
||||
}
|
||||
|
||||
message RequestingMember {
|
||||
bytes userId = 1;
|
||||
bytes profileKey = 2;
|
||||
bytes presentation = 3;
|
||||
uint64 timestamp = 4;
|
||||
}
|
||||
|
||||
message AccessControl {
|
||||
enum AccessRequired {
|
||||
UNKNOWN = 0;
|
||||
ANY = 1;
|
||||
MEMBER = 2;
|
||||
ADMINISTRATOR = 3;
|
||||
UNSATISFIABLE = 4;
|
||||
}
|
||||
|
||||
AccessRequired attributes = 1;
|
||||
AccessRequired members = 2;
|
||||
AccessRequired attributes = 1;
|
||||
AccessRequired members = 2;
|
||||
AccessRequired addFromInviteLink = 3;
|
||||
}
|
||||
|
||||
message Group {
|
||||
bytes publicKey = 1;
|
||||
bytes title = 2;
|
||||
string avatar = 3;
|
||||
bytes disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 revision = 6;
|
||||
repeated Member members = 7;
|
||||
repeated PendingMember pendingMembers = 8;
|
||||
bytes publicKey = 1;
|
||||
bytes title = 2;
|
||||
string avatar = 3;
|
||||
bytes disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 revision = 6;
|
||||
repeated Member members = 7;
|
||||
repeated PendingMember pendingMembers = 8;
|
||||
repeated RequestingMember requestingMembers = 9;
|
||||
bytes inviteLinkPassword = 10;
|
||||
}
|
||||
|
||||
message GroupChange {
|
||||
|
@ -65,7 +77,8 @@ message GroupChange {
|
|||
message Actions {
|
||||
|
||||
message AddMemberAction {
|
||||
Member added = 1;
|
||||
Member added = 1;
|
||||
bool joinFromInviteLink = 2;
|
||||
}
|
||||
|
||||
message DeleteMemberAction {
|
||||
|
@ -93,6 +106,19 @@ message GroupChange {
|
|||
bytes presentation = 1;
|
||||
}
|
||||
|
||||
message AddRequestingMemberAction {
|
||||
RequestingMember added = 1;
|
||||
}
|
||||
|
||||
message DeleteRequestingMemberAction {
|
||||
bytes deletedUserId = 1;
|
||||
}
|
||||
|
||||
message PromoteRequestingMemberAction {
|
||||
bytes userId = 1;
|
||||
Member.Role role = 2;
|
||||
}
|
||||
|
||||
message ModifyTitleAction {
|
||||
bytes title = 1;
|
||||
}
|
||||
|
@ -113,20 +139,33 @@ message GroupChange {
|
|||
AccessControl.AccessRequired membersAccess = 1;
|
||||
}
|
||||
|
||||
bytes sourceUuid = 1;
|
||||
uint32 revision = 2;
|
||||
repeated AddMemberAction addMembers = 3;
|
||||
repeated DeleteMemberAction deleteMembers = 4;
|
||||
repeated ModifyMemberRoleAction modifyMemberRoles = 5;
|
||||
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
|
||||
repeated AddPendingMemberAction addPendingMembers = 7;
|
||||
repeated DeletePendingMemberAction deletePendingMembers = 8;
|
||||
repeated PromotePendingMemberAction promotePendingMembers = 9;
|
||||
ModifyTitleAction modifyTitle = 10;
|
||||
ModifyAvatarAction modifyAvatar = 11;
|
||||
ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12;
|
||||
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
|
||||
ModifyMembersAccessControlAction modifyMemberAccess = 14;
|
||||
message ModifyAddFromInviteLinkAccessControlAction {
|
||||
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
|
||||
}
|
||||
|
||||
message ModifyInviteLinkPasswordAction {
|
||||
bytes inviteLinkPassword = 1;
|
||||
}
|
||||
|
||||
bytes sourceUuid = 1;
|
||||
uint32 revision = 2;
|
||||
repeated AddMemberAction addMembers = 3;
|
||||
repeated DeleteMemberAction deleteMembers = 4;
|
||||
repeated ModifyMemberRoleAction modifyMemberRoles = 5;
|
||||
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
|
||||
repeated AddPendingMemberAction addPendingMembers = 7;
|
||||
repeated DeletePendingMemberAction deletePendingMembers = 8;
|
||||
repeated PromotePendingMemberAction promotePendingMembers = 9;
|
||||
ModifyTitleAction modifyTitle = 10;
|
||||
ModifyAvatarAction modifyAvatar = 11;
|
||||
ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12;
|
||||
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
|
||||
ModifyMembersAccessControlAction modifyMemberAccess = 14;
|
||||
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15;
|
||||
repeated AddRequestingMemberAction addRequestingMembers = 16;
|
||||
repeated DeleteRequestingMemberAction deleteRequestingMembers = 17;
|
||||
repeated PromoteRequestingMemberAction promoteRequestingMembers = 18;
|
||||
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19;
|
||||
}
|
||||
|
||||
bytes actions = 1;
|
||||
|
@ -161,3 +200,12 @@ message GroupInviteLink {
|
|||
GroupInviteLinkContentsV1 v1Contents = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GroupJoinInfo {
|
||||
bytes publicKey = 1;
|
||||
bytes title = 2;
|
||||
string avatar = 3;
|
||||
uint32 memberCount = 4;
|
||||
AccessControl.AccessRequired addFromInviteLink = 5;
|
||||
uint32 revision = 6;
|
||||
}
|
||||
|
|
|
@ -5,16 +5,19 @@ import com.google.protobuf.ByteString;
|
|||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
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 org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -23,12 +26,28 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin
|
|||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class DecryptedGroupUtil_apply_Test {
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be applied by {@link DecryptedGroupUtil#apply}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_DecryptedGroupUtil_knows_about_all_fields_of_DecryptedGroupChange() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
|
||||
|
||||
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
|
||||
19, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_revision() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
|
@ -580,4 +599,168 @@ public final class DecryptedGroupUtil_apply_Test {
|
|||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invite_link_access() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(10)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build())
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(11)
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(11)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build())
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_new_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
|
||||
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(10)
|
||||
.addRequestingMembers(member1)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(11)
|
||||
.addNewRequestingMembers(member2)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(11)
|
||||
.addRequestingMembers(member1)
|
||||
.addRequestingMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_remove_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
|
||||
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(13)
|
||||
.addRequestingMembers(member1)
|
||||
.addRequestingMembers(member2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(14)
|
||||
.addDeleteRequestingMembers(member1.getUuid())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(14)
|
||||
.addRequestingMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promote_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
|
||||
UUID uuid1 = UUID.randomUUID();
|
||||
UUID uuid2 = UUID.randomUUID();
|
||||
UUID uuid3 = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = newProfileKey();
|
||||
ProfileKey profileKey2 = newProfileKey();
|
||||
ProfileKey profileKey3 = newProfileKey();
|
||||
DecryptedRequestingMember member1 = requestingMember(uuid1, profileKey1);
|
||||
DecryptedRequestingMember member2 = requestingMember(uuid2, profileKey2);
|
||||
DecryptedRequestingMember member3 = requestingMember(uuid3, profileKey3);
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(13)
|
||||
.addRequestingMembers(member1)
|
||||
.addRequestingMembers(member2)
|
||||
.addRequestingMembers(member3)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(14)
|
||||
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(member1.getUuid()))
|
||||
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.setUuid(member2.getUuid()))
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(14)
|
||||
.addMembers(member(uuid1, profileKey1))
|
||||
.addMembers(admin(uuid2, profileKey2))
|
||||
.addRequestingMembers(member3)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
|
||||
public void cannot_apply_promote_requesting_member_without_a_role() throws NotAbleToApplyGroupV2ChangeException {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
DecryptedRequestingMember member = requestingMember(uuid);
|
||||
|
||||
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(13)
|
||||
.addRequestingMembers(member)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(14)
|
||||
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setUuid(member.getUuid()))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invite_link_password() throws NotAbleToApplyGroupV2ChangeException {
|
||||
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(10)
|
||||
.setInviteLinkPassword(password1)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(11)
|
||||
.setNewInviteLinkPassword(password2)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(11)
|
||||
.setInviteLinkPassword(password2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invite_link_password_not_changed() throws NotAbleToApplyGroupV2ChangeException {
|
||||
ByteString password = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(10)
|
||||
.setInviteLinkPassword(password)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(11)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(11)
|
||||
.setInviteLinkPassword(password)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
@ -31,7 +35,7 @@ public final class DecryptedGroupUtil_empty_Test {
|
|||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
|
||||
|
||||
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
|
||||
DecryptedGroupUtil.MAX_CHANGE_FIELD, maxFieldFound);
|
||||
19, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -158,4 +162,54 @@ public final class DecryptedGroupUtil_empty_Test {
|
|||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_add_from_invite_link_access_field_15() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_an_add_requesting_member_field_16() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.addNewRequestingMembers(DecryptedRequestingMember.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_a_delete_requesting_member_field_17() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.addDeleteRequestingMembers(ByteString.copyFrom(new byte[16]))
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_a_promote_requesting_member_field_18() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.addPromoteRequestingMembers(DecryptedApproveMember.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_a_new_invite_link_password_19() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkPassword(ByteString.copyFrom(new byte[16]))
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
|
@ -8,21 +10,40 @@ 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 org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class GroupChangeReconstructTest {
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be detected by {@link GroupChangeReconstruct#reconstructGroupChange}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_GroupChangeReconstruct_knows_about_all_fields_of_DecryptedGroup() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
|
||||
|
||||
assertEquals("GroupChangeReconstruct and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
|
||||
10, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void empty_to_empty() {
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder().build();
|
||||
|
@ -219,4 +240,122 @@ public final class GroupChangeReconstructTest {
|
|||
|
||||
assertEquals(DecryptedGroupChange.newBuilder().addModifiedProfileKeys(withProfileKey(admin(uuid),profileKey2)).build(), decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_invite_access() {
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = newProfileKey();
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member1, profileKey1))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.addNewRequestingMembers(requestingMember(member1, profileKey1))
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_members_ignores_existing_by_uuid() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = newProfileKey();
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member1, newProfileKey()))
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member1, newProfileKey()))
|
||||
.addRequestingMembers(requestingMember(member2, profileKey2))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.addNewRequestingMembers(requestingMember(member2, profileKey2))
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removed_requesting_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member1, newProfileKey()))
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.addDeleteRequestingMembers(UuidUtil.toByteString(member1))
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promote_requesting_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = newProfileKey();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = newProfileKey();
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member1, profileKey1))
|
||||
.addRequestingMembers(requestingMember(member2, profileKey2))
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1, profileKey1))
|
||||
.addMembers(admin(member2, profileKey2))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.addPromoteRequestingMembers(approveMember(member1))
|
||||
.addPromoteRequestingMembers(approveAdmin(member2))
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_invite_link_password() {
|
||||
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder()
|
||||
.setInviteLinkPassword(password1)
|
||||
.build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder()
|
||||
.setInviteLinkPassword(password2)
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkPassword(password2)
|
||||
.build(),
|
||||
decryptedGroupChange);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import static org.junit.Assert.assertFalse;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class GroupChangeUtilTest {
|
||||
public final class GroupChangeUtil_changeIsEmpty_Test {
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
|
@ -20,7 +20,7 @@ public final class GroupChangeUtilTest {
|
|||
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
|
||||
|
||||
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
|
||||
GroupChangeUtil.CHANGE_ACTION_MAX_FIELD, maxFieldFound);
|
||||
19, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -31,7 +31,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_add_member_field_3() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -40,7 +40,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_delete_member_field_4() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -49,7 +49,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_member_roles_field_5() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().getDefaultInstanceForType())
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -58,7 +58,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_profile_keys_field_6() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().getDefaultInstanceForType())
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -67,7 +67,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_add_pending_members_field_7() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -76,7 +76,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_delete_pending_members_field_8() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -85,7 +85,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_promote_delete_pending_members_field_9() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -94,7 +94,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_title_field_10() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder().getDefaultInstanceForType())
|
||||
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -103,7 +103,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_avatar_field_11() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().getDefaultInstanceForType())
|
||||
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -112,7 +112,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_disappearing_message_timer_field_12() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().getDefaultInstanceForType())
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -121,7 +121,7 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_attributes_field_13() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder().getDefaultInstanceForType())
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
|
@ -130,7 +130,52 @@ public final class GroupChangeUtilTest {
|
|||
@Test
|
||||
public void not_empty_with_modify_member_access_field_14() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder().getDefaultInstanceForType())
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_add_from_invite_link_field_15() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_add_requesting_members_field_16() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_delete_requesting_members_field_17() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_promote_requesting_members_field_18() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_promote_requesting_members_field_19() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
|
@ -13,24 +13,69 @@ 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 org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encrypt;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedRequestingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class GroupChangeUtil_resolveConflict_Test {
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroupChange() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
|
||||
|
||||
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
|
||||
19, maxFieldFound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_resolveConflict_knows_about_all_fields_of_GroupChange() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
|
||||
|
||||
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(),
|
||||
19, maxFieldFound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroup() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
|
||||
|
||||
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
|
||||
10, maxFieldFound);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void empty_actions() {
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(DecryptedGroup.newBuilder().build(),
|
||||
|
@ -471,4 +516,160 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_15__no_membership_access_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder().setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction.newBuilder().setAddFromInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_16__changes_to_add_requesting_members_when_full_members_are_removed() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addMembers(member(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addNewRequestingMembers(requestingMember(member1))
|
||||
.addNewRequestingMembers(requestingMember(member2))
|
||||
.addNewRequestingMembers(requestingMember(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member1, randomProfileKey())))
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member3, randomProfileKey())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_16__changes_to_add_requesting_members_when_pending_are_promoted() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = randomProfileKey();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
ProfileKey profileKey3 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addPendingMembers(pendingMember(member1))
|
||||
.addPendingMembers(pendingMember(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addNewRequestingMembers(requestingMember(member1, profileKey1))
|
||||
.addNewRequestingMembers(requestingMember(member2, profileKey2))
|
||||
.addNewRequestingMembers(requestingMember(member3, profileKey3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member1, profileKey1)))
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member3, profileKey3)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member1, profileKey1)))
|
||||
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member3, profileKey3)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_17__changes_to_remove_missing_requesting_members_are_excluded() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(requestingMember(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addDeleteRequestingMembers(UuidUtil.toByteString(member1))
|
||||
.addDeleteRequestingMembers(UuidUtil.toByteString(member2))
|
||||
.addDeleteRequestingMembers(UuidUtil.toByteString(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member1)))
|
||||
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member3)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_18__promote_requesting_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addRequestingMembers(requestingMember(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addPromoteRequestingMembers(approveMember(member1))
|
||||
.addPromoteRequestingMembers(approveMember(member2))
|
||||
.addPromoteRequestingMembers(approveMember(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member1)))
|
||||
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member2)))
|
||||
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member3)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_19__password_change_is_kept() {
|
||||
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setInviteLinkPassword(password1)
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkPassword(password2)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder().setInviteLinkPassword(password2))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder().setInviteLinkPassword(password2))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
}
|
|
@ -8,11 +8,13 @@ import org.junit.Test;
|
|||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
|
@ -93,6 +95,23 @@ public final class GroupsV2Operations_decrypt_change_Test {
|
|||
.setUuid(UuidUtil.toByteString(newMember))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_decrypt_member_direct_join_field3() {
|
||||
UUID newMember = UUID.randomUUID();
|
||||
ProfileKey profileKey = newProfileKey();
|
||||
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
|
||||
|
||||
assertDecryption(groupOperations.createGroupJoinDirect(groupCandidate.getProfileKeyCredential().get())
|
||||
.setRevision(10),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(10)
|
||||
.addNewMembers(DecryptedMember.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.setJoinedAtRevision(10)
|
||||
.setUuid(UuidUtil.toByteString(newMember))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_decrypt_member_additions_direct_to_admin_field3() {
|
||||
UUID self = UUID.randomUUID();
|
||||
|
@ -266,20 +285,80 @@ public final class GroupsV2Operations_decrypt_change_Test {
|
|||
|
||||
@Test
|
||||
public void can_pass_through_new_attribute_access_rights_field_13() {
|
||||
assertDecryption(GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder()
|
||||
.setAttributesAccess(AccessControl.AccessRequired.MEMBER)),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER));
|
||||
assertDecryption(groupOperations.createChangeAttributesRights(AccessControl.AccessRequired.MEMBER),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_pass_through_new_membership_rights_field_14() {
|
||||
assertDecryption(groupOperations.createChangeMembershipRights(AccessControl.AccessRequired.ADMINISTRATOR),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_pass_through_new_add_by_invite_link_rights_field_15() {
|
||||
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.ADMINISTRATOR),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_pass_through_new_add_by_invite_link_rights_field_15_unsatisfiable() {
|
||||
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.UNSATISFIABLE),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_decrypt_member_requests_field16() {
|
||||
UUID newRequestingMember = UUID.randomUUID();
|
||||
ProfileKey profileKey = newProfileKey();
|
||||
GroupCandidate groupCandidate = groupCandidate(newRequestingMember, profileKey);
|
||||
|
||||
assertDecryption(groupOperations.createGroupJoinRequest(groupCandidate.getProfileKeyCredential().get())
|
||||
.setRevision(10),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(10)
|
||||
.addNewRequestingMembers(DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(newRequestingMember))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_decrypt_member_requests_refusals_field17() {
|
||||
UUID newRequestingMember = UUID.randomUUID();
|
||||
|
||||
assertDecryption(groupOperations.createRefuseGroupJoinRequest(Collections.singleton(newRequestingMember))
|
||||
.setRevision(10),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(10)
|
||||
.addDeleteRequestingMembers(UuidUtil.toByteString(newRequestingMember)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_decrypt_promote_requesting_members_field18() {
|
||||
UUID newRequestingMember = UUID.randomUUID();
|
||||
|
||||
assertDecryption(groupOperations.createApproveGroupJoinRequest(Collections.singleton(newRequestingMember))
|
||||
.setRevision(15),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(15)
|
||||
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(UuidUtil.toByteString(newRequestingMember))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_pass_through_new_invite_link_password_field19() {
|
||||
byte[] newPassword = Util.getSecretBytes(16);
|
||||
|
||||
assertDecryption(GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder()
|
||||
.setMembersAccess(AccessControl.AccessRequired.ADMINISTRATOR)),
|
||||
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder()
|
||||
.setInviteLinkPassword(ByteString.copyFrom(newPassword))),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
|
||||
.setNewInviteLinkPassword(ByteString.copyFrom(newPassword)));
|
||||
}
|
||||
|
||||
private static ProfileKey newProfileKey() {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.testutil.ZkGroupLibraryUtil;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class GroupsV2Operations_decrypt_groupJoinInfo_Test {
|
||||
|
||||
private GroupsV2Operations.GroupOperations groupOperations;
|
||||
|
||||
@Before
|
||||
public void setup() throws InvalidInputException {
|
||||
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
|
||||
|
||||
TestZkGroupServer server = new TestZkGroupServer();
|
||||
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
|
||||
|
||||
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroupJoinInfo}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(GroupJoinInfo.class);
|
||||
|
||||
assertEquals("GroupOperations and its tests need updating to account for new fields on " + GroupJoinInfo.class.getName(),
|
||||
6, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_title_field_2() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setTitle(groupOperations.encryptTitle("Title!"))
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals("Title!", decryptedGroupJoinInfo.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void avatar_field_passed_through_3() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setAvatar("AvatarCdnKey")
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals("AvatarCdnKey", decryptedGroupJoinInfo.getAvatar());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_count_passed_through_4() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setMemberCount(97)
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals(97, decryptedGroupJoinInfo.getMemberCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_from_invite_link_access_control_passed_though_5_administrator() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals(AccessControl.AccessRequired.ADMINISTRATOR, decryptedGroupJoinInfo.getAddFromInviteLink());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_from_invite_link_access_control_passed_though_5_any() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals(AccessControl.AccessRequired.ANY, decryptedGroupJoinInfo.getAddFromInviteLink());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void revision_passed_though_6() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setRevision(11)
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals(11, decryptedGroupJoinInfo.getRevision());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.PendingMember;
|
||||
import org.signal.storageservice.protos.groups.RequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.testutil.ZkGroupLibraryUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
|
||||
|
||||
public final class GroupsV2Operations_decrypt_group_Test {
|
||||
|
||||
private GroupSecretParams groupSecretParams;
|
||||
private GroupsV2Operations.GroupOperations groupOperations;
|
||||
|
||||
@Before
|
||||
public void setup() throws InvalidInputException {
|
||||
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
|
||||
|
||||
TestZkGroupServer server = new TestZkGroupServer();
|
||||
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
|
||||
|
||||
groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
|
||||
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroup}.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
|
||||
int maxFieldFound = getMaxDeclaredFieldNumber(Group.class);
|
||||
|
||||
assertEquals("GroupOperations and its tests need updating to account for new fields on " + Group.class.getName(),
|
||||
10, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_title_field_2() throws VerificationFailedException, InvalidGroupStateException {
|
||||
Group group = Group.newBuilder()
|
||||
.setTitle(groupOperations.encryptTitle("Title!"))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals("Title!", decryptedGroup.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void avatar_field_passed_through_3() throws VerificationFailedException, InvalidGroupStateException {
|
||||
Group group = Group.newBuilder()
|
||||
.setAvatar("AvatarCdnKey")
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals("AvatarCdnKey", decryptedGroup.getAvatar());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_message_timer_field_4() throws VerificationFailedException, InvalidGroupStateException {
|
||||
Group group = Group.newBuilder()
|
||||
.setDisappearingMessagesTimer(groupOperations.encryptTimer(123))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(123, decryptedGroup.getDisappearingMessagesTimer().getDuration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pass_through_access_control_field_5() throws VerificationFailedException, InvalidGroupStateException {
|
||||
AccessControl accessControl = AccessControl.newBuilder()
|
||||
.setMembers(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
Group group = Group.newBuilder()
|
||||
.setAccessControl(accessControl)
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(accessControl, decryptedGroup.getAccessControl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_revision_field_6() throws VerificationFailedException, InvalidGroupStateException {
|
||||
Group group = Group.newBuilder()
|
||||
.setRevision(99)
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(99, decryptedGroup.getRevision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_full_members_field_7() throws VerificationFailedException, InvalidGroupStateException {
|
||||
UUID admin1 = UUID.randomUUID();
|
||||
UUID member1 = UUID.randomUUID();
|
||||
ProfileKey adminProfileKey = newProfileKey();
|
||||
ProfileKey memberProfileKey = newProfileKey();
|
||||
|
||||
Group group = Group.newBuilder()
|
||||
.addMembers(Member.newBuilder()
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.setUserId(groupOperations.encryptUuid(admin1))
|
||||
.setJoinedAtRevision(4)
|
||||
.setProfileKey(encryptProfileKey(admin1, adminProfileKey)))
|
||||
.addMembers(Member.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUserId(groupOperations.encryptUuid(member1))
|
||||
.setJoinedAtRevision(7)
|
||||
.setProfileKey(encryptProfileKey(member1, memberProfileKey)))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.addMembers(DecryptedMember.newBuilder()
|
||||
.setJoinedAtRevision(4)
|
||||
.setUuid(UuidUtil.toByteString(admin1))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.setProfileKey(ByteString.copyFrom(adminProfileKey.serialize())))
|
||||
.addMembers(DecryptedMember.newBuilder()
|
||||
.setJoinedAtRevision(7)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(UuidUtil.toByteString(member1))
|
||||
.setProfileKey(ByteString.copyFrom(memberProfileKey.serialize())))
|
||||
.build().getMembersList(),
|
||||
decryptedGroup.getMembersList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_pending_members_field_8() throws VerificationFailedException, InvalidGroupStateException {
|
||||
UUID admin1 = UUID.randomUUID();
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID inviter1 = UUID.randomUUID();
|
||||
UUID inviter2 = UUID.randomUUID();
|
||||
|
||||
Group group = Group.newBuilder()
|
||||
.addPendingMembers(PendingMember.newBuilder()
|
||||
.setAddedByUserId(groupOperations.encryptUuid(inviter1))
|
||||
.setTimestamp(100)
|
||||
.setMember(Member.newBuilder()
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.setUserId(groupOperations.encryptUuid(admin1))))
|
||||
.addPendingMembers(PendingMember.newBuilder()
|
||||
.setAddedByUserId(groupOperations.encryptUuid(inviter1))
|
||||
.setTimestamp(200)
|
||||
.setMember(Member.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUserId(groupOperations.encryptUuid(member1))))
|
||||
.addPendingMembers(PendingMember.newBuilder()
|
||||
.setAddedByUserId(groupOperations.encryptUuid(inviter2))
|
||||
.setTimestamp(1500)
|
||||
.setMember(Member.newBuilder()
|
||||
.setUserId(groupOperations.encryptUuid(member2))))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.addPendingMembers(DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(admin1))
|
||||
.setUuidCipherText(groupOperations.encryptUuid(admin1))
|
||||
.setTimestamp(100)
|
||||
.setAddedByUuid(UuidUtil.toByteString(inviter1))
|
||||
.setRole(Member.Role.ADMINISTRATOR))
|
||||
.addPendingMembers(DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member1))
|
||||
.setUuidCipherText(groupOperations.encryptUuid(member1))
|
||||
.setTimestamp(200)
|
||||
.setAddedByUuid(UuidUtil.toByteString(inviter1))
|
||||
.setRole(Member.Role.DEFAULT))
|
||||
.addPendingMembers(DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member2))
|
||||
.setUuidCipherText(groupOperations.encryptUuid(member2))
|
||||
.setTimestamp(1500)
|
||||
.setAddedByUuid(UuidUtil.toByteString(inviter2))
|
||||
.setRole(Member.Role.DEFAULT))
|
||||
.build().getPendingMembersList(),
|
||||
decryptedGroup.getPendingMembersList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_requesting_members_field_9() throws VerificationFailedException, InvalidGroupStateException {
|
||||
UUID admin1 = UUID.randomUUID();
|
||||
UUID member1 = UUID.randomUUID();
|
||||
ProfileKey adminProfileKey = newProfileKey();
|
||||
ProfileKey memberProfileKey = newProfileKey();
|
||||
|
||||
Group group = Group.newBuilder()
|
||||
.addRequestingMembers(RequestingMember.newBuilder()
|
||||
.setUserId(groupOperations.encryptUuid(admin1))
|
||||
.setProfileKey(encryptProfileKey(admin1, adminProfileKey))
|
||||
.setTimestamp(5000))
|
||||
.addRequestingMembers(RequestingMember.newBuilder()
|
||||
.setUserId(groupOperations.encryptUuid(member1))
|
||||
.setProfileKey(encryptProfileKey(member1, memberProfileKey))
|
||||
.setTimestamp(15000))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.addRequestingMembers(DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(admin1))
|
||||
.setProfileKey(ByteString.copyFrom(adminProfileKey.serialize()))
|
||||
.setTimestamp(5000))
|
||||
.addRequestingMembers(DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member1))
|
||||
.setProfileKey(ByteString.copyFrom(memberProfileKey.serialize()))
|
||||
.setTimestamp(15000))
|
||||
.build().getRequestingMembersList(),
|
||||
decryptedGroup.getRequestingMembersList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pass_through_group_link_password_field_10() throws VerificationFailedException, InvalidGroupStateException {
|
||||
ByteString password = ByteString.copyFrom(Util.getSecretBytes(16));
|
||||
Group group = Group.newBuilder()
|
||||
.setInviteLinkPassword(password)
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals(password, decryptedGroup.getInviteLinkPassword());
|
||||
}
|
||||
|
||||
private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) {
|
||||
return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize());
|
||||
}
|
||||
|
||||
private static ProfileKey newProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,13 +3,17 @@ package org.whispersystems.signalservice.api.groupsv2;
|
|||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.RequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
@ -70,6 +74,12 @@ final class ProtoTestUtils {
|
|||
.build();
|
||||
}
|
||||
|
||||
static RequestingMember encryptedRequestingMember(UUID uuid, ProfileKey profileKey) {
|
||||
return RequestingMember.newBuilder()
|
||||
.setPresentation(presentation(uuid, profileKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember member(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
|
@ -101,6 +111,32 @@ final class ProtoTestUtils {
|
|||
.build();
|
||||
}
|
||||
|
||||
static DecryptedRequestingMember requestingMember(UUID uuid) {
|
||||
return requestingMember(uuid, newProfileKey());
|
||||
}
|
||||
|
||||
static DecryptedRequestingMember requestingMember(UUID uuid, ProfileKey profileKey) {
|
||||
return DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedApproveMember approveMember(UUID uuid) {
|
||||
return approve(uuid, Member.Role.DEFAULT);
|
||||
}
|
||||
|
||||
static DecryptedApproveMember approveAdmin(UUID uuid) {
|
||||
return approve(uuid, Member.Role.ADMINISTRATOR);
|
||||
}
|
||||
|
||||
private static DecryptedApproveMember approve(UUID uuid, Member.Role role) {
|
||||
return DecryptedApproveMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(role)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
|
||||
return withProfileKey(member(uuid), profileKey);
|
||||
}
|
||||
|
@ -135,4 +171,12 @@ final class ProtoTestUtils {
|
|||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ProfileKey newProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
package org.whispersystems.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
Loading…
Add table
Reference in a new issue