Apply server returned group patch instead of local only.

fork-5.53.8
Cody Henthorne 2022-02-25 14:22:17 -05:00 zatwierdzone przez Alex Hart
rodzic 2d7655a6bb
commit 69dc31681d
9 zmienionych plików z 352 dodań i 73 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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