Group invite link epoch support.

This commit is contained in:
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

@@ -1,38 +0,0 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import org.whispersystems.util.Base64;
import java.io.IOException;
public final class Base64UrlSafe {
private Base64UrlSafe() {
}
public static @NonNull byte[] decode(@NonNull String s) throws IOException {
return Base64.decode(s, Base64.URL_SAFE);
}
public static @NonNull byte[] decodePaddingAgnostic(@NonNull String s) throws IOException {
switch (s.length() % 4) {
case 1:
case 3: s = s + "="; break;
case 2: s = s + "=="; break;
}
return decode(s);
}
public static @NonNull String encodeBytes(@NonNull byte[] source) {
try {
return Base64.encodeBytes(source, Base64.URL_SAFE);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static @NonNull String encodeBytesWithoutPadding(@NonNull byte[] source) {
return encodeBytes(source).replace("=", "");
}
}

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

@@ -1,66 +0,0 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public final class Base64UrlSafeTest {
private final byte[] data;
private final String encoded;
private final String encodedWithoutPadding;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{ "", "", "" },
{ "01", "AQ==", "AQ" },
{ "0102", "AQI=", "AQI" },
{ "010203", "AQID", "AQID" },
{ "030405", "AwQF", "AwQF" },
{ "03040506", "AwQFBg==", "AwQFBg" },
{ "0304050708", "AwQFBwg=", "AwQFBwg" },
{ "af4d6cff", "r01s_w==", "r01s_w" },
{ "ffefde", "_-_e", "_-_e" },
});
}
public Base64UrlSafeTest(String hexData, String encoded, String encodedWithoutPadding) throws IOException {
this.data = Hex.fromStringCondensed(hexData);
this.encoded = encoded;
this.encodedWithoutPadding = encodedWithoutPadding;
}
@Test
public void encodes_as_expected() {
assertEquals(encoded, Base64UrlSafe.encodeBytes(data));
}
@Test
public void encodes_as_expected_without_padding() {
assertEquals(encodedWithoutPadding, Base64UrlSafe.encodeBytesWithoutPadding(data));
}
@Test
public void decodes_as_expected() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decode(encoded));
}
@Test
public void decodes_padding_agnostic_as_expected() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encoded));
}
@Test
public void decodes_as_expected_without_padding() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encodedWithoutPadding));
}
}