kopia lustrzana https://github.com/ryukoposting/Signal-Android
Apply server returned group patch instead of local only.
rodzic
2d7655a6bb
commit
69dc31681d
|
@ -161,7 +161,7 @@ public final class GroupManager {
|
||||||
throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException
|
throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException
|
||||||
{
|
{
|
||||||
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
edit.ejectMember(recipient.getId(), false);
|
edit.ejectMember(recipient.requireServiceId(), false);
|
||||||
Log.i(TAG, "Member removed from group " + groupId);
|
Log.i(TAG, "Member removed from group " + groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import com.annimon.stream.Collectors;
|
import com.annimon.stream.Collectors;
|
||||||
|
@ -97,16 +98,39 @@ final class GroupManagerV2 {
|
||||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||||
private final ACI selfAci;
|
private final ACI selfAci;
|
||||||
private final GroupCandidateHelper groupCandidateHelper;
|
private final GroupCandidateHelper groupCandidateHelper;
|
||||||
|
private final SendGroupUpdateHelper sendGroupUpdateHelper;
|
||||||
|
|
||||||
GroupManagerV2(@NonNull Context context) {
|
GroupManagerV2(@NonNull Context context) {
|
||||||
|
this(context,
|
||||||
|
SignalDatabase.groups(),
|
||||||
|
ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api(),
|
||||||
|
ApplicationDependencies.getGroupsV2Operations(),
|
||||||
|
ApplicationDependencies.getGroupsV2Authorization(),
|
||||||
|
ApplicationDependencies.getGroupsV2StateProcessor(),
|
||||||
|
SignalStore.account().requireAci(),
|
||||||
|
new GroupCandidateHelper(context),
|
||||||
|
new SendGroupUpdateHelper(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting GroupManagerV2(Context context,
|
||||||
|
GroupDatabase groupDatabase,
|
||||||
|
GroupsV2Api groupsV2Api,
|
||||||
|
GroupsV2Operations groupsV2Operations,
|
||||||
|
GroupsV2Authorization authorization,
|
||||||
|
GroupsV2StateProcessor groupsV2StateProcessor,
|
||||||
|
ACI selfAci,
|
||||||
|
GroupCandidateHelper groupCandidateHelper,
|
||||||
|
SendGroupUpdateHelper sendGroupUpdateHelper)
|
||||||
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.groupDatabase = SignalDatabase.groups();
|
this.groupDatabase = groupDatabase;
|
||||||
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
|
this.groupsV2Api = groupsV2Api;
|
||||||
this.groupsV2Operations = ApplicationDependencies.getGroupsV2Operations();
|
this.groupsV2Operations = groupsV2Operations;
|
||||||
this.authorization = ApplicationDependencies.getGroupsV2Authorization();
|
this.authorization = authorization;
|
||||||
this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor();
|
this.groupsV2StateProcessor = groupsV2StateProcessor;
|
||||||
this.selfAci = SignalStore.account().requireAci();
|
this.selfAci = selfAci;
|
||||||
this.groupCandidateHelper = new GroupCandidateHelper(context);
|
this.groupCandidateHelper = groupCandidateHelper;
|
||||||
|
this.sendGroupUpdateHelper = sendGroupUpdateHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull DecryptedGroupJoinInfo getGroupJoinInfoFromServer(@NonNull GroupMasterKey groupMasterKey, @Nullable GroupLinkPassword password)
|
@NonNull DecryptedGroupJoinInfo getGroupJoinInfoFromServer(@NonNull GroupMasterKey groupMasterKey, @Nullable GroupLinkPassword password)
|
||||||
|
@ -234,7 +258,7 @@ final class GroupManagerV2 {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
void sendNoopGroupUpdate(@NonNull GroupMasterKey masterKey, @NonNull DecryptedGroup currentState) {
|
void sendNoopGroupUpdate(@NonNull GroupMasterKey masterKey, @NonNull DecryptedGroup currentState) {
|
||||||
sendGroupUpdate(masterKey, new GroupMutation(currentState, DecryptedGroupChange.newBuilder().build(), currentState), null);
|
sendGroupUpdateHelper.sendGroupUpdate(masterKey, new GroupMutation(currentState, DecryptedGroupChange.newBuilder().build(), currentState), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,7 +297,7 @@ final class GroupManagerV2 {
|
||||||
.setEditor(selfAci.toByteString())
|
.setEditor(selfAci.toByteString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
|
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
|
||||||
|
|
||||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
|
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
|
||||||
recipientAndThread.threadId,
|
recipientAndThread.threadId,
|
||||||
|
@ -420,7 +444,6 @@ final class GroupManagerV2 {
|
||||||
@NonNull GroupManager.GroupActionResult leaveGroup()
|
@NonNull GroupManager.GroupActionResult leaveGroup()
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
{
|
{
|
||||||
Recipient self = Recipient.self();
|
|
||||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.getGroup(groupId).get();
|
GroupDatabase.GroupRecord groupRecord = groupDatabase.getGroup(groupId).get();
|
||||||
List<DecryptedPendingMember> pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList();
|
List<DecryptedPendingMember> pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList();
|
||||||
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
|
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
|
||||||
|
@ -432,17 +455,15 @@ final class GroupManagerV2 {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ejectMember(self.getId(), true);
|
return ejectMember(ServiceId.from(selfAci.uuid()), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull RecipientId recipientId, boolean allowWhenBlocked)
|
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked)
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
{
|
{
|
||||||
Recipient recipient = Recipient.resolved(recipientId);
|
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid())), allowWhenBlocked);
|
||||||
|
|
||||||
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.requireServiceId().uuid())), allowWhenBlocked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@ -633,20 +654,21 @@ final class GroupManagerV2 {
|
||||||
throw new GroupChangeFailedException("Group is blocked.");
|
throw new GroupChangeFailedException("Group is blocked.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousGroupState = v2GroupProperties.getDecryptedGroup();
|
||||||
|
|
||||||
|
GroupChange signedGroupChange = commitToServer(changeActions);
|
||||||
try {
|
try {
|
||||||
previousGroupState = v2GroupProperties.getDecryptedGroup();
|
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
|
||||||
decryptedChange = groupOperations.decryptChange(changeActions, selfAci.uuid());
|
|
||||||
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
|
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
|
||||||
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupChange signedGroupChange = commitToServer(changeActions);
|
|
||||||
groupDatabase.update(groupId, decryptedGroupState);
|
groupDatabase.update(groupId, decryptedGroupState);
|
||||||
|
|
||||||
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
|
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
|
||||||
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange);
|
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange);
|
||||||
int newMembersCount = decryptedChange.getNewMembersCount();
|
int newMembersCount = decryptedChange.getNewMembersCount();
|
||||||
List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList());
|
List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList());
|
||||||
|
|
||||||
|
@ -862,7 +884,7 @@ final class GroupManagerV2 {
|
||||||
} else if (requestToJoin) {
|
} else if (requestToJoin) {
|
||||||
Log.i(TAG, "Requested to join, cannot send update");
|
Log.i(TAG, "Requested to join, cannot send update");
|
||||||
|
|
||||||
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange, false);
|
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange, false);
|
||||||
|
|
||||||
return new GroupManager.GroupActionResult(groupRecipient,
|
return new GroupManager.GroupActionResult(groupRecipient,
|
||||||
recipientAndThread.threadId,
|
recipientAndThread.threadId,
|
||||||
|
@ -887,7 +909,7 @@ final class GroupManagerV2 {
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
decryptedChange);
|
decryptedChange);
|
||||||
|
|
||||||
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
|
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
|
||||||
|
|
||||||
return new GroupManager.GroupActionResult(groupRecipient,
|
return new GroupManager.GroupActionResult(groupRecipient,
|
||||||
recipientAndThread.threadId,
|
recipientAndThread.threadId,
|
||||||
|
@ -1086,7 +1108,7 @@ final class GroupManagerV2 {
|
||||||
|
|
||||||
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.getRevision()));
|
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.getRevision()));
|
||||||
|
|
||||||
sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false);
|
sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false);
|
||||||
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
||||||
throw new GroupChangeFailedException(e);
|
throw new GroupChangeFailedException(e);
|
||||||
}
|
}
|
||||||
|
@ -1139,55 +1161,65 @@ final class GroupManagerV2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
@VisibleForTesting
|
||||||
@NonNull GroupMutation groupMutation,
|
static class SendGroupUpdateHelper {
|
||||||
@Nullable GroupChange signedGroupChange)
|
|
||||||
{
|
|
||||||
return sendGroupUpdate(masterKey, groupMutation, signedGroupChange, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
private final Context context;
|
||||||
@NonNull GroupMutation groupMutation,
|
|
||||||
@Nullable GroupChange signedGroupChange,
|
SendGroupUpdateHelper(Context context) {
|
||||||
boolean sendToMembers)
|
this.context = context;
|
||||||
{
|
}
|
||||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
|
||||||
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
@NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
||||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
|
@NonNull GroupMutation groupMutation,
|
||||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
@Nullable GroupChange signedGroupChange)
|
||||||
decryptedGroupV2Context,
|
{
|
||||||
null,
|
return sendGroupUpdate(masterKey, groupMutation, signedGroupChange, true);
|
||||||
System.currentTimeMillis(),
|
}
|
||||||
0,
|
|
||||||
false,
|
@NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
||||||
null,
|
@NonNull GroupMutation groupMutation,
|
||||||
Collections.emptyList(),
|
@Nullable GroupChange signedGroupChange,
|
||||||
Collections.emptyList(),
|
boolean sendToMembers)
|
||||||
Collections.emptyList());
|
{
|
||||||
|
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||||
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
|
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
|
||||||
|
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
||||||
|
decryptedGroupV2Context,
|
||||||
|
null,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList());
|
||||||
|
|
||||||
|
|
||||||
DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();
|
DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();
|
||||||
|
|
||||||
if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) {
|
if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) {
|
||||||
if (sendToMembers) {
|
if (sendToMembers) {
|
||||||
ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, groupMutation.getNewGroupState(), outgoingMessage));
|
ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, groupMutation.getNewGroupState(), outgoingMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RecipientAndThread(groupRecipient, -1);
|
return new RecipientAndThread(groupRecipient, -1);
|
||||||
} else {
|
} else {
|
||||||
if (sendToMembers) {
|
if (sendToMembers) {
|
||||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null, null);
|
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null, null);
|
||||||
return new RecipientAndThread(groupRecipient, threadId);
|
return new RecipientAndThread(groupRecipient, threadId);
|
||||||
} else {
|
} else {
|
||||||
long threadId = SignalDatabase.threads().getOrCreateValidThreadId(outgoingMessage.getRecipient(), -1, outgoingMessage.getDistributionType());
|
long threadId = SignalDatabase.threads().getOrCreateValidThreadId(outgoingMessage.getRecipient(), -1, outgoingMessage.getDistributionType());
|
||||||
try {
|
try {
|
||||||
long messageId = SignalDatabase.mms().insertMessageOutbox(outgoingMessage, threadId, false, null);
|
long messageId = SignalDatabase.mms().insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||||
SignalDatabase.mms().markAsSent(messageId, true);
|
SignalDatabase.mms().markAsSent(messageId, true);
|
||||||
SignalDatabase.threads().update(threadId, true);
|
SignalDatabase.threads().update(threadId, true);
|
||||||
} catch (MmsException e) {
|
} catch (MmsException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
return new RecipientAndThread(groupRecipient, threadId);
|
||||||
}
|
}
|
||||||
return new RecipientAndThread(groupRecipient, threadId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public final class GroupCandidateHelper {
|
public class GroupCandidateHelper {
|
||||||
private final SignalServiceAccountManager signalServiceAccountManager;
|
private final SignalServiceAccountManager signalServiceAccountManager;
|
||||||
private final RecipientDatabase recipientDatabase;
|
private final RecipientDatabase recipientDatabase;
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ import java.util.stream.Collectors;
|
||||||
/**
|
/**
|
||||||
* Advances a groups state to a specified revision.
|
* Advances a groups state to a specified revision.
|
||||||
*/
|
*/
|
||||||
public final class GroupsV2StateProcessor {
|
public class GroupsV2StateProcessor {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(GroupsV2StateProcessor.class);
|
private static final String TAG = Log.tag(GroupsV2StateProcessor.class);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.ServerPublicParams;
|
||||||
|
import org.signal.zkgroup.ServerSecretParams;
|
||||||
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.groups.GroupPublicParams;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||||
|
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||||
|
import org.whispersystems.signalservice.test.LibSignalLibraryUtil;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Zk group operations that the server would provide.
|
||||||
|
* Copied in app from libsignal
|
||||||
|
*/
|
||||||
|
public final class TestZkGroupServer {
|
||||||
|
|
||||||
|
private final ServerPublicParams serverPublicParams;
|
||||||
|
private final ServerZkProfileOperations serverZkProfileOperations;
|
||||||
|
|
||||||
|
public TestZkGroupServer() {
|
||||||
|
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
|
||||||
|
|
||||||
|
ServerSecretParams serverSecretParams = ServerSecretParams.generate();
|
||||||
|
|
||||||
|
serverPublicParams = serverSecretParams.getPublicParams();
|
||||||
|
serverZkProfileOperations = new ServerZkProfileOperations(serverSecretParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerPublicParams getServerPublicParams() {
|
||||||
|
return serverPublicParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse(ProfileKeyCredentialRequest request, UUID uuid, ProfileKeyCommitment commitment) throws VerificationFailedException {
|
||||||
|
return serverZkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertProfileKeyCredentialPresentation(GroupPublicParams publicParams, ProfileKeyCredentialPresentation profileKeyCredentialPresentation) {
|
||||||
|
try {
|
||||||
|
serverZkProfileOperations.verifyProfileKeyCredentialPresentation(publicParams, profileKeyCredentialPresentation);
|
||||||
|
} catch (VerificationFailedException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.signal.storageservice.protos.groups.AccessControl
|
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.Member
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||||
|
@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.libsignal.util.guava.Optional
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
|
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
@ -69,12 +71,39 @@ class ChangeSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupStateTestData(private val masterKey: GroupMasterKey) {
|
class GroupChangeData(private val revision: Int, private val groupOperations: GroupsV2Operations.GroupOperations) {
|
||||||
|
private val groupChangeBuilder: GroupChange.Builder = GroupChange.newBuilder()
|
||||||
|
private val actionsBuilder: GroupChange.Actions.Builder = GroupChange.Actions.newBuilder()
|
||||||
|
var changeEpoch: Int = GroupsV2Operations.HIGHEST_KNOWN_EPOCH
|
||||||
|
|
||||||
|
val groupChange: GroupChange
|
||||||
|
get() {
|
||||||
|
return groupChangeBuilder
|
||||||
|
.setChangeEpoch(changeEpoch)
|
||||||
|
.setActions(actionsBuilder.setRevision(revision).build().toByteString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun source(serviceId: ServiceId) {
|
||||||
|
actionsBuilder.sourceUuid = groupOperations.encryptUuid(serviceId.uuid())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMember(serviceId: ServiceId) {
|
||||||
|
actionsBuilder.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().setDeletedUserId(groupOperations.encryptUuid(serviceId.uuid())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyRole(serviceId: ServiceId, role: Member.Role) {
|
||||||
|
actionsBuilder.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(groupOperations.encryptUuid(serviceId.uuid())).setRole(role))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupStateTestData(private val masterKey: GroupMasterKey, private val groupOperations: GroupsV2Operations.GroupOperations? = null) {
|
||||||
|
|
||||||
var localState: DecryptedGroup? = null
|
var localState: DecryptedGroup? = null
|
||||||
var groupRecord: Optional<GroupDatabase.GroupRecord> = Optional.absent()
|
var groupRecord: Optional<GroupDatabase.GroupRecord> = Optional.absent()
|
||||||
var serverState: DecryptedGroup? = null
|
var serverState: DecryptedGroup? = null
|
||||||
var changeSet: ChangeSet? = null
|
var changeSet: ChangeSet? = null
|
||||||
|
var groupChange: GroupChange? = null
|
||||||
var includeFirst: Boolean = false
|
var includeFirst: Boolean = false
|
||||||
var requestedRevision: Int = 0
|
var requestedRevision: Int = 0
|
||||||
|
|
||||||
|
@ -120,6 +149,12 @@ class GroupStateTestData(private val masterKey: GroupMasterKey) {
|
||||||
this.requestedRevision = requestedRevision
|
this.requestedRevision = requestedRevision
|
||||||
this.includeFirst = includeFirst
|
this.includeFirst = includeFirst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun groupChange(revision: Int, init: GroupChangeData.() -> Unit) {
|
||||||
|
val groupChangeData = GroupChangeData(revision, groupOperations!!)
|
||||||
|
groupChangeData.init()
|
||||||
|
this.groupChange = groupChangeData.groupChange
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun groupRecord(
|
fun groupRecord(
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
@file:Suppress("ClassName")
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.hamcrest.Matchers.`is`
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.core.util.ThreadUtil
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.storageservice.protos.groups.Member
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.signal.zkgroup.groups.GroupSecretParams
|
||||||
|
import org.thoughtcrime.securesms.SignalStoreRule
|
||||||
|
import org.thoughtcrime.securesms.TestZkGroupServer
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||||
|
import org.thoughtcrime.securesms.database.member
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
|
||||||
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||||
|
import org.thoughtcrime.securesms.util.Hex
|
||||||
|
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||||
|
import org.whispersystems.signalservice.api.push.ACI
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(manifest = Config.NONE, application = Application::class)
|
||||||
|
class GroupManagerV2Test_edit {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val server: TestZkGroupServer = TestZkGroupServer()
|
||||||
|
val masterKey: GroupMasterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||||
|
val groupSecretParams: GroupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey)
|
||||||
|
val groupId: GroupId.V2 = GroupId.v2(masterKey)
|
||||||
|
|
||||||
|
val selfAci: ACI = ACI.from(UUID.randomUUID())
|
||||||
|
val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
|
||||||
|
val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
|
||||||
|
val others: List<DecryptedMember> = listOf(member(otherSid))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var groupDatabase: GroupDatabase
|
||||||
|
private lateinit var groupsV2API: GroupsV2Api
|
||||||
|
private lateinit var groupsV2Operations: GroupsV2Operations
|
||||||
|
private lateinit var groupsV2Authorization: GroupsV2Authorization
|
||||||
|
private lateinit var groupsV2StateProcessor: GroupsV2StateProcessor
|
||||||
|
private lateinit var groupCandidateHelper: GroupCandidateHelper
|
||||||
|
private lateinit var sendGroupUpdateHelper: GroupManagerV2.SendGroupUpdateHelper
|
||||||
|
private lateinit var groupOperations: GroupsV2Operations.GroupOperations
|
||||||
|
|
||||||
|
private lateinit var patchedDecryptedGroup: ArgumentCaptor<DecryptedGroup>
|
||||||
|
|
||||||
|
private lateinit var manager: GroupManagerV2
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val signalStore: SignalStoreRule = SignalStoreRule()
|
||||||
|
|
||||||
|
@Suppress("UsePropertyAccessSyntax")
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
ThreadUtil.enforceAssertions = false
|
||||||
|
Log.initialize(SystemOutLogger())
|
||||||
|
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
||||||
|
|
||||||
|
val clientZkOperations = ClientZkOperations(server.getServerPublicParams())
|
||||||
|
|
||||||
|
groupDatabase = mock(GroupDatabase::class.java)
|
||||||
|
groupsV2API = mock(GroupsV2Api::class.java)
|
||||||
|
groupsV2Operations = GroupsV2Operations(clientZkOperations)
|
||||||
|
groupsV2Authorization = mock(GroupsV2Authorization::class.java)
|
||||||
|
groupsV2StateProcessor = mock(GroupsV2StateProcessor::class.java)
|
||||||
|
groupCandidateHelper = mock(GroupCandidateHelper::class.java)
|
||||||
|
sendGroupUpdateHelper = mock(GroupManagerV2.SendGroupUpdateHelper::class.java)
|
||||||
|
groupOperations = groupsV2Operations.forGroup(groupSecretParams)
|
||||||
|
|
||||||
|
patchedDecryptedGroup = ArgumentCaptor.forClass(DecryptedGroup::class.java)
|
||||||
|
|
||||||
|
manager = GroupManagerV2(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
groupDatabase,
|
||||||
|
groupsV2API,
|
||||||
|
groupsV2Operations,
|
||||||
|
groupsV2Authorization,
|
||||||
|
groupsV2StateProcessor,
|
||||||
|
selfAci,
|
||||||
|
groupCandidateHelper,
|
||||||
|
sendGroupUpdateHelper
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun given(init: GroupStateTestData.() -> Unit) {
|
||||||
|
val data = GroupStateTestData(masterKey, groupOperations)
|
||||||
|
data.init()
|
||||||
|
|
||||||
|
Mockito.doReturn(data.groupRecord).`when`(groupDatabase).getGroup(groupId)
|
||||||
|
Mockito.doReturn(data.groupRecord.get()).`when`(groupDatabase).requireGroup(groupId)
|
||||||
|
|
||||||
|
Mockito.doReturn(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any())
|
||||||
|
|
||||||
|
Mockito.doReturn(data.groupChange!!).`when`(groupsV2API).patchGroup(Mockito.any(), Mockito.any(), Mockito.any())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editGroup(perform: GroupManagerV2.GroupEditor.() -> Unit) {
|
||||||
|
manager.edit(groupId).use { it.perform() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun then(then: (DecryptedGroup) -> Unit) {
|
||||||
|
Mockito.verify(groupDatabase).update(Mockito.eq(groupId), patchedDecryptedGroup.capture())
|
||||||
|
then(patchedDecryptedGroup.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when you are the only admin, and then leave the group, server upgrades all other members to administrators and lets you leave`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 5,
|
||||||
|
members = listOf(
|
||||||
|
member(selfAci, role = Member.Role.ADMINISTRATOR),
|
||||||
|
member(otherSid)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
groupChange(6) {
|
||||||
|
source(selfAci)
|
||||||
|
deleteMember(selfAci)
|
||||||
|
modifyRole(otherSid, Member.Role.ADMINISTRATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editGroup {
|
||||||
|
leaveGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
then { patchedGroup ->
|
||||||
|
assertThat("Revision updated by one", patchedGroup.revision, `is`(6))
|
||||||
|
assertThat("Self is no longer in the group", patchedGroup.membersList.find { it.uuid == selfAci.toByteString() }, Matchers.nullValue())
|
||||||
|
assertThat("Other is now an admin in the group", patchedGroup.membersList.find { it.uuid == otherSid.toByteString() }?.role, `is`(Member.Role.ADMINISTRATOR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import org.signal.core.util.concurrent.TracingExecutor;
|
import org.signal.core.util.concurrent.TracingExecutor;
|
||||||
import org.signal.core.util.concurrent.TracingExecutorService;
|
import org.signal.core.util.concurrent.TracingExecutorService;
|
||||||
|
@ -19,6 +20,9 @@ public final class ThreadUtil {
|
||||||
|
|
||||||
private static volatile Handler handler;
|
private static volatile Handler handler;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static volatile boolean enforceAssertions = true;
|
||||||
|
|
||||||
private ThreadUtil() {}
|
private ThreadUtil() {}
|
||||||
|
|
||||||
private static Handler getHandler() {
|
private static Handler getHandler() {
|
||||||
|
@ -37,13 +41,13 @@ public final class ThreadUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assertMainThread() {
|
public static void assertMainThread() {
|
||||||
if (!isMainThread()) {
|
if (!isMainThread() && enforceAssertions) {
|
||||||
throw new AssertionError("Must run on main thread.");
|
throw new AssertionError("Must run on main thread.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assertNotMainThread() {
|
public static void assertNotMainThread() {
|
||||||
if (isMainThread()) {
|
if (isMainThread() && enforceAssertions) {
|
||||||
throw new AssertionError("Cannot run on main thread.");
|
throw new AssertionError("Cannot run on main thread.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -732,7 +732,8 @@ public final class GroupsV2Operations {
|
||||||
return UuidUtil.toByteString(decryptUuid(userId));
|
return UuidUtil.toByteString(decryptUuid(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteString encryptUuid(UUID uuid) {
|
// Visible for Testing
|
||||||
|
public ByteString encryptUuid(UUID uuid) {
|
||||||
return ByteString.copyFrom(clientZkGroupCipher.encryptUuid(uuid).serialize());
|
return ByteString.copyFrom(clientZkGroupCipher.encryptUuid(uuid).serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue