Group invite link epoch support.

master
Alan Evans 2020-08-18 14:26:09 -03:00 committed by Greyson Parrelli
parent e006306036
commit 477bb45df7
31 changed files with 2366 additions and 205 deletions

View File

@ -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 {
/**

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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)));
}
}

View File

@ -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'
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View File

@ -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() {
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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;

View File

@ -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 {
}

View File

@ -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("=", "");
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;