mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Apply server returned group patch instead of local only.
This commit is contained in:
committed by
Alex Hart
parent
2d7655a6bb
commit
69dc31681d
@@ -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 org.signal.storageservice.protos.groups.AccessControl
|
||||
import org.signal.storageservice.protos.groups.GroupChange
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
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.signalservice.api.groupsv2.DecryptedGroupHistoryEntry
|
||||
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.ServiceId
|
||||
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 groupRecord: Optional<GroupDatabase.GroupRecord> = Optional.absent()
|
||||
var serverState: DecryptedGroup? = null
|
||||
var changeSet: ChangeSet? = null
|
||||
var groupChange: GroupChange? = null
|
||||
var includeFirst: Boolean = false
|
||||
var requestedRevision: Int = 0
|
||||
|
||||
@@ -120,6 +149,12 @@ class GroupStateTestData(private val masterKey: GroupMasterKey) {
|
||||
this.requestedRevision = requestedRevision
|
||||
this.includeFirst = includeFirst
|
||||
}
|
||||
|
||||
fun groupChange(revision: Int, init: GroupChangeData.() -> Unit) {
|
||||
val groupChangeData = GroupChangeData(revision, groupOperations!!)
|
||||
groupChangeData.init()
|
||||
this.groupChange = groupChangeData.groupChange
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user