Add ability to reject group invite by PNI.

This commit is contained in:
Cody Henthorne
2022-04-25 12:37:25 -04:00
parent e22560a794
commit 657a9c7b0a
41 changed files with 626 additions and 325 deletions

View File

@@ -590,7 +590,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setTitle("Clear all profile keys?")
.setMessage("Are you sure? Never do this on a non-test device.")
.setPositiveButton(android.R.string.ok) { _, _ ->
SignalDatabase.recipients.debugClearServiceIds()
SignalDatabase.recipients.debugClearProfileData()
Toast.makeText(context, "Cleared all profile keys.", Toast.LENGTH_SHORT).show()
}
.setNegativeButton(android.R.string.cancel) { d, _ ->

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.Hex
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -169,6 +171,28 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
)
}
clickPref(
title = DSLSettingsText.from("Clear recipient data"),
summary = DSLSettingsText.from("Clears service id, profile data, sessions, identities, and thread."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.hasServiceId()) {
SignalDatabase.recipients.debugClearServiceIds(recipient.id)
SignalDatabase.recipients.debugClearProfileData(recipient.id)
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireServiceId().toString())
ApplicationDependencies.getProtocolStore().pni().identities().delete(recipient.requireServiceId().toString())
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
}
startActivity(MainActivity.clearTop(requireContext()))
}
.show()
}
)
if (recipient.isSelf) {
sectionHeaderPref(DSLSettingsText.from("Donations"))

View File

@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.util.Stopwatch
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.services.CdshV2Service
import java.io.IOException
import java.lang.NumberFormatException
import java.util.Optional
/**
@@ -90,7 +89,7 @@ object ContactDiscoveryRefreshV2 {
val inputIds: Set<RecipientId> = recipients.map { it.id }.toSet()
val inputE164s: Set<String> = recipients.mapNotNull { it.e164.orElse(null) }.toSet()
if (recipients.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.")
val fullResult: ContactDiscovery.RefreshResult = refreshAll(context)
@@ -100,7 +99,12 @@ object ContactDiscoveryRefreshV2 {
)
}
Log.i(TAG, "Doing a one-off request for ${recipients.size} recipients.")
if (inputE164s.isEmpty()) {
Log.w(TAG, "No numbers to refresh!")
return ContactDiscovery.RefreshResult(emptySet(), emptyMap())
} else {
Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.")
}
val response: CdshV2Service.Response = makeRequest(
previousE164s = emptySet(),

View File

@@ -13,7 +13,9 @@ import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
@@ -33,8 +35,6 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
@@ -83,6 +83,7 @@ public class GroupDatabase extends Database {
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
private static final String DISTRIBUTION_ID = "distribution_id";
private static final String DISPLAY_AS_STORY = "display_as_story";
private static final String AUTH_SERVICE_ID = "auth_service_id";
/* V2 Group columns */
@@ -112,18 +113,19 @@ public class GroupDatabase extends Database {
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
DISTRIBUTION_ID + " TEXT DEFAULT NULL, " +
DISPLAY_AS_STORY + " INTEGER DEFAULT 0);";
DISPLAY_AS_STORY + " INTEGER DEFAULT 0, " +
AUTH_SERVICE_ID + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON " + TABLE_NAME + "(" + DISTRIBUTION_ID + ");"
};
};
private static final String[] GROUP_PROJECTION = {
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, AUTH_SERVICE_ID
};
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
@@ -459,23 +461,25 @@ private static final String[] GROUP_PROJECTION = {
if (groupExists(groupId.deriveV2MigrationGroupId())) {
throw new LegacyGroupInsertException(groupId);
}
create(groupId, title, members, avatar, relay, null, null);
create(null, groupId, title, members, avatar, relay, null, null);
}
public void create(@NonNull GroupId.Mms groupId,
@Nullable String title,
@NonNull Collection<RecipientId> members)
{
create(groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null);
create(null, groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null);
}
public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey,
public GroupId.V2 create(@Nullable ServiceId authServiceId,
@NonNull GroupMasterKey groupMasterKey,
@NonNull DecryptedGroup groupState)
{
return create(groupMasterKey, groupState, false);
return create(authServiceId, groupMasterKey, groupState, false);
}
public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey,
public GroupId.V2 create(@Nullable ServiceId authServiceId,
@NonNull GroupMasterKey groupMasterKey,
@NonNull DecryptedGroup groupState,
boolean force)
{
@@ -487,7 +491,7 @@ private static final String[] GROUP_PROJECTION = {
Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!");
}
create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
create(authServiceId, groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
return groupId;
}
@@ -496,7 +500,7 @@ private static final String[] GROUP_PROJECTION = {
* There was a point in time where we weren't properly responding to group creates on linked devices. This would result in us having a Recipient entry for the
* group, but we'd either be missing the group entry, or that entry would be missing a master key. This method fixes this scenario.
*/
public void fixMissingMasterKey(@NonNull GroupMasterKey groupMasterKey) {
public void fixMissingMasterKey(@Nullable ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) {
GroupId.V2 groupId = GroupId.v2(groupMasterKey);
if (getGroupV1ByExpectedV2(groupId).isPresent()) {
@@ -517,7 +521,8 @@ private static final String[] GROUP_PROJECTION = {
if (updated < 1) {
Log.w(TAG, "No group entry. Creating restore placeholder for " + groupId);
create(groupMasterKey,
create(authServiceId,
groupMasterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build(),
@@ -538,7 +543,8 @@ private static final String[] GROUP_PROJECTION = {
/**
* @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version).
*/
private void create(@NonNull GroupId groupId,
private void create(@Nullable ServiceId authServiceId,
@NonNull GroupId groupId,
@Nullable String title,
@NonNull Collection<RecipientId> memberCollection,
@Nullable SignalServiceAttachmentPointer avatar,
@@ -553,6 +559,7 @@ private static final String[] GROUP_PROJECTION = {
Collections.sort(members);
ContentValues contentValues = new ContentValues();
contentValues.put(AUTH_SERVICE_ID, authServiceId != null ? authServiceId.toString() : null);
contentValues.put(RECIPIENT_ID, groupRecipientId.serialize());
contentValues.put(GROUP_ID, groupId.toString());
contentValues.put(TITLE, title);
@@ -964,6 +971,13 @@ private static final String[] GROUP_PROJECTION = {
return result;
}
public void setAuthServiceId(@Nullable ServiceId authServiceId, @NonNull GroupId groupId) {
ContentValues values = new ContentValues(1);
values.put(AUTH_SERVICE_ID, authServiceId == null ? null : authServiceId.toString());
getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId));
}
public static class Reader implements Closeable {
public final Cursor cursor;
@@ -1008,7 +1022,8 @@ private static final String[] GROUP_PROJECTION = {
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
CursorUtil.requireInt(cursor, V2_REVISION),
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null));
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null),
CursorUtil.requireString(cursor, AUTH_SERVICE_ID));
}
@Override
@@ -1034,6 +1049,7 @@ private static final String[] GROUP_PROJECTION = {
private final boolean mms;
@Nullable private final V2GroupProperties v2GroupProperties;
private final DistributionId distributionId;
@Nullable private final String authServiceId;
public GroupRecord(@NonNull GroupId id,
@NonNull RecipientId recipientId,
@@ -1050,7 +1066,8 @@ private static final String[] GROUP_PROJECTION = {
@Nullable byte[] groupMasterKeyBytes,
int groupRevision,
@Nullable byte[] decryptedGroupBytes,
@Nullable DistributionId distributionId)
@Nullable DistributionId distributionId,
@Nullable String authServiceId)
{
this.id = id;
this.recipientId = recipientId;
@@ -1063,6 +1080,7 @@ private static final String[] GROUP_PROJECTION = {
this.active = active;
this.mms = mms;
this.distributionId = distributionId;
this.authServiceId = authServiceId;
V2GroupProperties v2GroupProperties = null;
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
@@ -1193,7 +1211,11 @@ private static final String[] GROUP_PROJECTION = {
public MemberLevel memberLevel(@NonNull Recipient recipient) {
if (isV2Group()) {
return requireV2GroupProperties().memberLevel(recipient);
MemberLevel memberLevel = requireV2GroupProperties().memberLevel(recipient.getServiceId());
if (recipient.isSelf() && memberLevel == MemberLevel.NOT_A_MEMBER) {
memberLevel = requireV2GroupProperties().memberLevel(Optional.ofNullable(SignalStore.account().getPni()));
}
return memberLevel;
} else if (isMms() && recipient.isSelf()) {
return MemberLevel.FULL_MEMBER;
} else {
@@ -1247,6 +1269,10 @@ private static final String[] GROUP_PROJECTION = {
}
return false;
}
public @Nullable ServiceId getAuthServiceId() {
return ServiceId.parseOrNull(authServiceId);
}
}
public static class V2GroupProperties {
@@ -1297,9 +1323,7 @@ private static final String[] GROUP_PROJECTION = {
return members.stream().filter(this::isAdmin).collect(Collectors.toList());
}
public MemberLevel memberLevel(@NonNull Recipient recipient) {
Optional<ServiceId> serviceId = recipient.getServiceId();
public MemberLevel memberLevel(@NonNull Optional<ServiceId> serviceId) {
if (!serviceId.isPresent()) {
return MemberLevel.NOT_A_MEMBER;
}

View File

@@ -953,6 +953,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
Log.i(TAG, "Creating restore placeholder for $groupId")
groups.create(
null,
masterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
@@ -1597,9 +1598,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return updated
}
private fun clearProfileKeyCredential(id: RecipientId) {
fun clearProfileKeyCredential(id: RecipientId) {
val values = ContentValues(1)
values.putNull(PROFILE_KEY_CREDENTIAL)
values.putNull(PROFILE_KEY)
values.put(PROFILE_SHARING, 0)
if (update(id, values)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
@@ -2938,28 +2941,45 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
/**
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
*/
fun debugClearServiceIds() {
fun debugClearServiceIds(recipientId: RecipientId? = null) {
writableDatabase
.update(TABLE_NAME)
.values(
SERVICE_ID to null,
PNI_COLUMN to null
)
.where("$ID != ?", Recipient.self().id)
.run {
if (recipientId == null) {
where("$ID != ?", Recipient.self().id)
} else {
where("$ID = ?", recipientId)
}
}
.run()
}
/**
* Should only be used for debugging! A very destructive action that clears all known profile keys and credentials.
*/
fun debugClearProfileKeys() {
fun debugClearProfileData(recipientId: RecipientId? = null) {
writableDatabase
.update(TABLE_NAME)
.values(
PROFILE_KEY to null,
PROFILE_KEY_CREDENTIAL to null
PROFILE_KEY_CREDENTIAL to null,
PROFILE_GIVEN_NAME to null,
PROFILE_FAMILY_NAME to null,
PROFILE_JOINED_NAME to null,
LAST_PROFILE_FETCH to 0,
SIGNAL_PROFILE_AVATAR to null
)
.where("$ID != ?", Recipient.self().id)
.run {
if (recipientId == null) {
where("$ID != ?", Recipient.self().id)
} else {
where("$ID = ?", recipientId)
}
}
.run()
}

View File

@@ -194,8 +194,9 @@ object SignalDatabaseMigrations {
private const val CLEAN_DELETED_DISTRIBUTION_LISTS = 138
private const val REMOVE_KNOWN_UNKNOWNS = 139
private const val CDS_V2 = 140
private const val GROUP_SERVICE_ID = 141
const val DATABASE_VERSION = 140
const val DATABASE_VERSION = 141
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -2523,6 +2524,10 @@ object SignalDatabaseMigrations {
"""
)
}
if (oldVersion < GROUP_SERVICE_ID) {
db.execSQL("ALTER TABLE groups ADD COLUMN auth_service_id TEXT DEFAULT NULL")
}
}
@JvmStatic

View File

@@ -1,12 +1,8 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.graphics.Color;
import android.text.PrecomputedText;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
@@ -17,6 +13,7 @@ import androidx.annotation.VisibleForTesting;
import com.google.protobuf.ByteString;
import org.signal.core.util.StringUtil;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
@@ -30,16 +27,15 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
import org.thoughtcrime.securesms.keyvalue.ServiceIds;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -55,15 +51,13 @@ import java.util.stream.Collectors;
final class GroupsV2UpdateMessageProducer {
@NonNull private final Context context;
@NonNull private final UUID selfUuid;
@NonNull private final ByteString selfUuidBytes;
@NonNull private final Context context;
@NonNull private final ServiceIds selfIds;
@Nullable private final Consumer<RecipientId> recipientClickHandler;
GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull UUID selfUuid, @Nullable Consumer<RecipientId> recipientClickHandler) {
GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull ServiceIds selfIds, @Nullable Consumer<RecipientId> recipientClickHandler) {
this.context = context;
this.selfUuid = selfUuid;
this.selfUuidBytes = UuidUtil.toByteString(selfUuid);
this.selfIds = selfIds;
this.recipientClickHandler = recipientClickHandler;
}
@@ -77,21 +71,25 @@ final class GroupsV2UpdateMessageProducer {
* When the revision of the group is 0, the change is very noisy and only the editor is useful.
*/
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) {
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfIds.getAci().uuid());
if (!selfPending.isPresent()) {
selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfIds.getPni().uuid());
}
if (selfPending.isPresent()) {
return updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, selfPending.get().getAddedByUuid(), R.drawable.ic_update_group_add_16);
}
ByteString foundingMemberUuid = decryptedGroupChange.getEditor();
if (!foundingMemberUuid.isEmpty()) {
if (selfUuidBytes.equals(foundingMemberUuid)) {
if (selfIds.matches(foundingMemberUuid)) {
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_16);
} else {
return updateDescription(R.string.MessageRecord_s_added_you, foundingMemberUuid, R.drawable.ic_update_group_add_16);
}
}
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getAci().uuid()).isPresent() || DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent()) {
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16);
} else {
return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16);
@@ -163,7 +161,7 @@ final class GroupsV2UpdateMessageProducer {
* Handles case of future protocol versions where we don't know what has changed.
*/
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16));
@@ -177,10 +175,10 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
for (DecryptedMember member : change.getNewMembersList()) {
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(member.getUuid());
if (editorIsYou) {
if (newMemberIsYou) {
@@ -204,7 +202,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (DecryptedMember member : change.getNewMembersList()) {
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(member.getUuid());
if (newMemberIsYou) {
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16));
@@ -215,10 +213,10 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
for (ByteString member : change.getDeleteMembersList()) {
boolean removedMemberIsYou = member.equals(selfUuidBytes);
boolean removedMemberIsYou = selfIds.matches(member);
if (editorIsYou) {
if (removedMemberIsYou) {
@@ -242,7 +240,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (ByteString member : change.getDeleteMembersList()) {
boolean removedMemberIsYou = member.equals(selfUuidBytes);
boolean removedMemberIsYou = selfIds.matches(member);
if (removedMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_16));
@@ -253,10 +251,10 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
boolean changedMemberIsYou = selfIds.matches(roleChange.getUuid());
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
if (editorIsYou) {
updates.add(updateDescription(R.string.MessageRecord_you_made_s_an_admin, roleChange.getUuid(), R.drawable.ic_update_group_role_16));
@@ -284,7 +282,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
boolean changedMemberIsYou = selfIds.matches(roleChange.getUuid());
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
if (changedMemberIsYou) {
@@ -303,11 +301,11 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
int notYouInviteCount = 0;
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(invitee.getUuid());
if (newMemberIsYou) {
updates.add(0, updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, change.getEditor(), R.drawable.ic_update_group_add_16));
@@ -321,8 +319,7 @@ final class GroupsV2UpdateMessageProducer {
}
if (notYouInviteCount > 0) {
final int notYouInviteCountFinalCopy = notYouInviteCount;
updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, change.getEditor(), notYouInviteCountFinalCopy, R.drawable.ic_update_group_add_16));
updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, notYouInviteCount, change.getEditor(), notYouInviteCount, R.drawable.ic_update_group_add_16));
}
}
@@ -330,7 +327,7 @@ final class GroupsV2UpdateMessageProducer {
int notYouInviteCount = 0;
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(invitee.getUuid());
if (newMemberIsYou) {
UUID uuid = UuidUtil.fromByteStringOrUnknown(invitee.getAddedByUuid());
@@ -351,7 +348,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
int notDeclineCount = 0;
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
@@ -362,7 +359,7 @@ final class GroupsV2UpdateMessageProducer {
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_16));
}
} else if (invitee.getUuid().equals(selfUuidBytes)) {
} else if (selfIds.matches(invitee.getUuid())) {
updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, change.getEditor(), R.drawable.ic_update_group_decline_16));
} else {
notDeclineCount++;
@@ -373,8 +370,7 @@ final class GroupsV2UpdateMessageProducer {
if (editorIsYou) {
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_16));
} else {
final int notDeclineCountFinalCopy = notDeclineCount;
updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, change.getEditor(), notDeclineCountFinalCopy, R.drawable.ic_update_group_decline_16));
updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notDeclineCount, change.getEditor(), notDeclineCount, R.drawable.ic_update_group_decline_16));
}
}
}
@@ -383,7 +379,7 @@ final class GroupsV2UpdateMessageProducer {
int notDeclineCount = 0;
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
boolean inviteeWasYou = selfIds.matches(invitee.getUuid());
if (inviteeWasYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_16));
@@ -398,11 +394,11 @@ final class GroupsV2UpdateMessageProducer {
}
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
ByteString uuid = newMember.getUuid();
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(uuid);
if (editorIsYou) {
if (newMemberIsYou) {
@@ -427,7 +423,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
ByteString uuid = newMember.getUuid();
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
boolean newMemberIsYou = selfIds.matches(uuid);
if (newMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16));
@@ -438,7 +434,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.hasNewTitle()) {
String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue());
@@ -451,7 +447,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeNewDescription(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.hasNewDescription()) {
if (editorIsYou) {
@@ -475,7 +471,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.hasNewAvatar()) {
if (editorIsYou) {
@@ -493,7 +489,7 @@ final class GroupsV2UpdateMessageProducer {
}
void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
@@ -513,7 +509,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
@@ -533,7 +529,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
@@ -562,7 +558,7 @@ final class GroupsV2UpdateMessageProducer {
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
}
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
boolean groupLinkEnabled = false;
switch (change.getNewInviteLinkAccess()) {
@@ -655,7 +651,7 @@ final class GroupsV2UpdateMessageProducer {
Set<ByteString> deleteRequestingUuids = new HashSet<>(change.getDeleteRequestingMembersList());
for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) {
boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes);
boolean requestingMemberIsYou = selfIds.matches(member.getUuid());
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_16));
@@ -675,12 +671,12 @@ final class GroupsV2UpdateMessageProducer {
private void describeRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
boolean requestingMemberIsYou = selfIds.matches(requestingMember.getUuid());
if (requestingMemberIsYou) {
updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, change.getEditor(), R.drawable.ic_update_group_accept_16));
} else {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (editorIsYou) {
updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16));
@@ -693,7 +689,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
boolean requestingMemberIsYou = selfIds.matches(requestingMember.getUuid());
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_16));
@@ -704,16 +700,16 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
Set<ByteString> newRequestingUuids = change.getNewRequestingMembersList().stream().map(r -> r.getUuid()).collect(Collectors.toSet());
Set<ByteString> newRequestingUuids = change.getNewRequestingMembersList().stream().map(DecryptedRequestingMember::getUuid).collect(Collectors.toSet());
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
if (newRequestingUuids.contains(requestingMember)) {
continue;
}
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
boolean requestingMemberIsYou = selfIds.matches(requestingMember);
if (requestingMemberIsYou) {
if (editorIsYou) {
@@ -735,7 +731,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
boolean requestingMemberIsYou = selfIds.matches(requestingMember);
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_16));
@@ -746,7 +742,7 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeAnnouncementGroupChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean editorIsYou = selfIds.matches(change.getEditor());
if (change.getNewIsAnnouncementGroup() == EnabledState.ENABLED) {
if (editorIsYou) {
@@ -858,8 +854,7 @@ final class GroupsV2UpdateMessageProducer {
}
private static @NonNull Object[] makePlaceholders(@NonNull List<RecipientId> recipientIds, @Nullable List<Object> formatArgs) {
List<String> placeholders = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList());
List<Object> args = new LinkedList<>(placeholders);
List<Object> args = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList());
if (formatArgs != null) {
args.addAll(formatArgs);

View File

@@ -33,6 +33,7 @@ import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
@@ -58,7 +59,6 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
@@ -270,7 +270,7 @@ public abstract class MessageRecord extends DisplayRecord {
try {
byte[] decoded = Base64.decode(body);
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().requireAci().uuid(), recipientClickHandler);
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().requireServiceIds(), recipientClickHandler);
if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));

View File

@@ -6,6 +6,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import org.signal.core.util.Hex;
import org.signal.core.util.concurrent.DeadlockDetector;
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
import org.thoughtcrime.securesms.KbsEnclave;
@@ -38,7 +39,6 @@ import org.thoughtcrime.securesms.shakereport.ShakeToReport;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.EarlyMessageCache;
import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.IasKeyStore;
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
@@ -152,8 +152,10 @@ public class ApplicationDependencies {
if (groupsV2Authorization == null) {
synchronized (LOCK) {
if (groupsV2Authorization == null) {
GroupsV2Authorization.ValueCache authCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AuthorizationCache());
groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), authCache);
GroupsV2Authorization.ValueCache aciAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache());
GroupsV2Authorization.ValueCache pniAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2PniAuthorizationCache());
groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), aciAuthCache, pniAuthCache);
}
}
}

View File

@@ -6,8 +6,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
@@ -24,7 +22,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
import java.util.Collection;
@@ -40,7 +38,8 @@ public final class GroupManager {
private static final String TAG = Log.tag(GroupManager.class);
@WorkerThread
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
public static @NonNull GroupActionResult createGroup(@NonNull ServiceId authServiceId,
@NonNull Context context,
@NonNull Set<Recipient> members,
@Nullable byte[] avatar,
@Nullable String name,
@@ -54,7 +53,7 @@ public final class GroupManager {
if (shouldAttemptToCreateV2) {
try {
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer);
return groupCreator.createGroup(authServiceId, memberIds, name, avatar, disappearingMessagesTimer);
}
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);
@@ -178,6 +177,7 @@ public final class GroupManager {
*/
@WorkerThread
public static void updateGroupFromServer(@NonNull Context context,
@NonNull ServiceId authServiceId,
@NonNull GroupMasterKey groupMasterKey,
int revision,
long timestamp,
@@ -185,17 +185,18 @@ public final class GroupManager {
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange);
updater.updateLocalToServerRevision(authServiceId, revision, timestamp, signedGroupChange);
}
}
@WorkerThread
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
@NonNull ServiceId authServiceserviceId,
@NonNull GroupMasterKey groupMasterKey)
throws IOException
{
try {
new GroupManagerV2(context).groupServerQuery(groupMasterKey);
new GroupManagerV2(context).groupServerQuery(authServiceserviceId, groupMasterKey);
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
} catch (GroupNotAMemberException e) {
return V2GroupServerStatus.NOT_A_MEMBER;
@@ -210,11 +211,12 @@ public final class GroupManager {
* If it fails to get the exact version, it will give the latest.
*/
@WorkerThread
public static DecryptedGroup addedGroupVersion(@NonNull Context context,
public static DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId,
@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey)
throws IOException, GroupDoesNotExistException, GroupNotAMemberException
{
return new GroupManagerV2(context).addedGroupVersion(groupMasterKey);
return new GroupManagerV2(context).addedGroupVersion(authServiceId, groupMasterKey);
}
@WorkerThread
@@ -268,12 +270,13 @@ public final class GroupManager {
@WorkerThread
public static void revokeInvites(@NonNull Context context,
@NonNull ServiceId authServiceId,
@NonNull GroupId.V2 groupId,
@NonNull Collection<UuidCiphertext> uuidCipherTexts)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.revokeInvites(uuidCipherTexts);
editor.revokeInvites(authServiceId, uuidCipherTexts, true);
}
}

View File

@@ -62,6 +62,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
@@ -97,6 +98,7 @@ final class GroupManagerV2 {
private final GroupsV2Authorization authorization;
private final GroupsV2StateProcessor groupsV2StateProcessor;
private final ACI selfAci;
private final PNI selfPni;
private final GroupCandidateHelper groupCandidateHelper;
private final SendGroupUpdateHelper sendGroupUpdateHelper;
@@ -108,6 +110,7 @@ final class GroupManagerV2 {
ApplicationDependencies.getGroupsV2Authorization(),
ApplicationDependencies.getGroupsV2StateProcessor(),
SignalStore.account().requireAci(),
SignalStore.account().requirePni(),
new GroupCandidateHelper(context),
new SendGroupUpdateHelper(context));
}
@@ -119,6 +122,7 @@ final class GroupManagerV2 {
GroupsV2Authorization authorization,
GroupsV2StateProcessor groupsV2StateProcessor,
ACI selfAci,
PNI selfPni,
GroupCandidateHelper groupCandidateHelper,
SendGroupUpdateHelper sendGroupUpdateHelper)
{
@@ -129,6 +133,7 @@ final class GroupManagerV2 {
this.authorization = authorization;
this.groupsV2StateProcessor = groupsV2StateProcessor;
this.selfAci = selfAci;
this.selfPni = selfPni;
this.groupCandidateHelper = groupCandidateHelper;
this.sendGroupUpdateHelper = sendGroupUpdateHelper;
}
@@ -204,18 +209,18 @@ final class GroupManagerV2 {
}
@WorkerThread
void groupServerQuery(@NonNull GroupMasterKey groupMasterKey)
void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
{
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
.getCurrentGroupStateFromServer();
}
@WorkerThread
@NonNull DecryptedGroup addedGroupVersion(@NonNull GroupMasterKey groupMasterKey)
@NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
{
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(groupMasterKey);
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey);
DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
if (latest.getRevision() == 0) {
@@ -233,7 +238,7 @@ final class GroupManagerV2 {
if (joinedVersion != null) {
return joinedVersion;
} else {
Log.w(TAG, "Unable to retreive exact version joined at, using latest");
Log.w(TAG, "Unable to retrieve exact version joined at, using latest");
return latest;
}
}
@@ -269,7 +274,8 @@ final class GroupManagerV2 {
}
@WorkerThread
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
@NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId,
@NonNull Collection<RecipientId> members,
@Nullable String name,
@Nullable byte[] avatar,
int disappearingMessagesTimer)
@@ -285,7 +291,7 @@ final class GroupManagerV2 {
}
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
GroupId.V2 groupId = groupDatabase.create(authServiceId, masterKey, decryptedGroup);
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
@@ -306,6 +312,7 @@ final class GroupManagerV2 {
}
}
@SuppressWarnings("UnusedReturnValue")
final class GroupEditor extends LockOwner {
private final GroupId.V2 groupId;
@@ -340,35 +347,35 @@ final class GroupManagerV2 {
groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates);
}
return commitChangeWithConflictResolution(groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid()));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime));
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupTimerChange(expirationTime));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateAttributesRights(@NonNull GroupAccessControl newRights)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateMembershipRights(@NonNull GroupAccessControl newRights)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateAnnouncementGroup(boolean isAnnouncementGroup)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
return commitChangeWithConflictResolution(selfAci, groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
}
@WorkerThread
@@ -390,7 +397,7 @@ final class GroupManagerV2 {
.setAvatar(cdnKey));
}
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change);
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(selfAci, change);
if (avatarChanged) {
AvatarHelper.setAvatar(context, Recipient.externalGroupExact(context, groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
@@ -404,10 +411,10 @@ final class GroupManagerV2 {
}
@WorkerThread
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts)
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection<UuidCiphertext> uuidCipherTexts, boolean sendToMembers)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)));
return commitChangeWithConflictResolution(authServiceId, groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)), false, sendToMembers);
}
@WorkerThread
@@ -418,7 +425,7 @@ final class GroupManagerV2 {
.map(r -> Recipient.resolved(r).requireServiceId().uuid())
.collect(Collectors.toSet());
return commitChangeWithConflictResolution(groupOperations.createApproveGroupJoinRequest(uuids));
return commitChangeWithConflictResolution(selfAci, groupOperations.createApproveGroupJoinRequest(uuids));
}
@WorkerThread
@@ -429,7 +436,7 @@ final class GroupManagerV2 {
.map(r -> Recipient.resolved(r).requireServiceId().uuid())
.collect(Collectors.toSet());
return commitChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
}
@WorkerThread
@@ -438,33 +445,47 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
Recipient recipient = Recipient.resolved(recipientId);
return commitChangeWithConflictResolution(groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult leaveGroup()
void leaveGroup()
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
GroupDatabase.GroupRecord groupRecord = groupDatabase.getGroup(groupId).get();
List<DecryptedPendingMember> pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList();
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
Optional<DecryptedMember> selfMember = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), selfAci.uuid());
Optional<DecryptedPendingMember> aciPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfAci.uuid());
Optional<DecryptedPendingMember> pniPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfPni.uuid());
Optional<DecryptedPendingMember> selfPendingMember = Optional.empty();
ServiceId serviceId = selfAci;
if (aciPendingMember.isPresent()) {
selfPendingMember = aciPendingMember;
} else if (pniPendingMember.isPresent() && !selfMember.isPresent()) {
selfPendingMember = pniPendingMember;
serviceId = selfPni;
}
if (selfPendingMember.isPresent()) {
try {
return revokeInvites(Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray())));
revokeInvites(serviceId, Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray())), false);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
} else if (selfMember.isPresent()) {
ejectMember(serviceId, true, false);
} else {
return ejectMember(ServiceId.from(selfAci.uuid()), true, false);
Log.i(TAG, "Unable to leave group we are not pending or in");
}
}
@WorkerThread
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked, boolean ban)
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId authServiceId, boolean allowWhenBlocked, boolean ban)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid()),
return commitChangeWithConflictResolution(authServiceId,
groupOperations.createRemoveMembersChange(Collections.singleton(authServiceId.uuid()),
ban,
ban ? v2GroupProperties.getDecryptedGroup().getBannedMembersList()
: Collections.emptyList()),
@@ -475,10 +496,9 @@ final class GroupManagerV2 {
@NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection<RecipientId> newAdmins)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
Recipient self = Recipient.self();
List<UUID> newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).requireServiceId().uuid()).toList();
return commitChangeWithConflictResolution(groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(),
return commitChangeWithConflictResolution(selfAci, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(),
newAdminRecipients));
}
@@ -510,7 +530,7 @@ final class GroupManagerV2 {
return null;
}
return commitChangeWithConflictResolution(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireProfileKeyCredential()));
}
@WorkerThread
@@ -532,7 +552,7 @@ final class GroupManagerV2 {
return null;
}
return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireProfileKeyCredential()));
}
public GroupManager.GroupActionResult ban(UUID uuid)
@@ -541,20 +561,20 @@ final class GroupManagerV2 {
ByteString uuidByteString = UuidUtil.toByteString(uuid);
boolean rejectJoinRequest = v2GroupProperties.getDecryptedGroup().getRequestingMembersList().stream().anyMatch(m -> m.getUuid().equals(uuidByteString));
return commitChangeWithConflictResolution(groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
}
public GroupManager.GroupActionResult unban(Set<UUID> uuids)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(groupOperations.createUnbanUuidsChange(uuids));
return commitChangeWithConflictResolution(selfAci, groupOperations.createUnbanUuidsChange(uuids));
}
@WorkerThread
public GroupManager.GroupActionResult cycleGroupLinkPassword()
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
}
@WorkerThread
@@ -581,7 +601,7 @@ final class GroupManagerV2 {
}
}
commitChangeWithConflictResolution(change);
commitChangeWithConflictResolution(selfAci, change);
if (state != GroupManager.GroupLinkState.DISABLED) {
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.requireGroup(groupId).requireV2GroupProperties();
@@ -594,26 +614,32 @@ final class GroupManagerV2 {
}
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(change, false);
return commitChangeWithConflictResolution(authServiceId, change, false);
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
change.setSourceUuid(UuidUtil.toByteString(selfAci.uuid()));
return commitChangeWithConflictResolution(authServiceId, change, allowWhenBlocked, true);
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
change.setSourceUuid(UuidUtil.toByteString(authServiceId.uuid()));
for (int attempt = 0; attempt < 5; attempt++) {
try {
return commitChange(change, allowWhenBlocked);
return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers);
} catch (GroupPatchNotAcceptedException e) {
throw new GroupChangeFailedException(e);
} catch (ConflictException e) {
Log.w(TAG, "Invalid group patch or conflict", e);
change = resolveConflict(change);
change = resolveConflict(authServiceId, change);
if (GroupChangeUtil.changeIsEmpty(change.build())) {
Log.i(TAG, "Change is empty after conflict resolution");
@@ -628,10 +654,10 @@ final class GroupManagerV2 {
throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
}
private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions.Builder change)
private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
throws IOException, GroupNotAMemberException, GroupChangeFailedException
{
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(authServiceId, groupMasterKey)
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
if (groupUpdateResult.getLatestServer() == null) {
@@ -652,14 +678,14 @@ final class GroupManagerV2 {
GroupChange.Actions changeActions = change.build();
return GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
groupOperations.decryptChange(changeActions, selfAci.uuid()),
groupOperations.decryptChange(changeActions, authServiceId.uuid()),
changeActions);
} catch (VerificationFailedException | InvalidGroupStateException ex) {
throw new GroupChangeFailedException(ex);
}
}
private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
@@ -676,8 +702,9 @@ final class GroupManagerV2 {
previousGroupState = v2GroupProperties.getDecryptedGroup();
GroupChange signedGroupChange = commitToServer(changeActions);
GroupChange signedGroupChange = commitToServer(authServiceId, changeActions);
try {
//noinspection OptionalGetWithoutIsPresent
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
@@ -688,18 +715,18 @@ final class GroupManagerV2 {
groupDatabase.update(groupId, decryptedGroupState);
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange);
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers);
int newMembersCount = decryptedChange.getNewMembersCount();
List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList());
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
}
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
private @NonNull GroupChange commitToServer(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions change)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{
try {
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfAci, groupSecretParams), Optional.empty());
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(authServiceId, groupSecretParams), Optional.empty());
} catch (NotInGroupException e) {
Log.w(TAG, e);
throw new GroupNotAMemberException(e);
@@ -724,10 +751,10 @@ final class GroupManagerV2 {
}
@WorkerThread
void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange)
void updateLocalToServerRevision(@NonNull ServiceId authServiceId, int revision, long timestamp, @Nullable byte[] signedGroupChange)
throws IOException, GroupNotAMemberException
{
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
}
@@ -880,7 +907,7 @@ final class GroupManagerV2 {
groupDatabase.update(groupId, updatedGroup);
} else {
groupDatabase.create(groupMasterKey, decryptedGroup);
groupDatabase.create(selfAci, groupMasterKey, decryptedGroup);
Log.i(TAG, "Created local group with placeholder");
}
@@ -924,7 +951,7 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, IOException
{
try {
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
new GroupsV2StateProcessor(context).forGroup(selfAci, groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.getRevision(),
System.currentTimeMillis(),
decryptedChange);
@@ -956,6 +983,7 @@ final class GroupManagerV2 {
throws GroupChangeFailedException
{
try {
//noinspection OptionalGetWithoutIsPresent
return groupOperations.decryptChange(signedGroupChange, false).get();
} catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
Log.w(TAG, e);
@@ -1006,7 +1034,7 @@ final class GroupManagerV2 {
throw new MembershipNotSuitableForV2Exception("No profile key credential for self");
}
ProfileKeyCredential profileKeyCredential = self.getProfileKeyCredential().get();
ProfileKeyCredential profileKeyCredential = self.requireProfileKeyCredential();
GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential)
: groupOperations.createGroupJoinDirect(profileKeyCredential);
@@ -1123,6 +1151,7 @@ final class GroupManagerV2 {
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
try {
//noinspection OptionalGetWithoutIsPresent
DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange);
@@ -1226,6 +1255,7 @@ final class GroupManagerV2 {
return new RecipientAndThread(groupRecipient, -1);
} else {
//noinspection IfStatementWithIdenticalBranches
if (sendToMembers) {
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null, null);
return new RecipientAndThread(groupRecipient, threadId);

View File

@@ -2,10 +2,23 @@ package org.thoughtcrime.securesms.groups;
public final class GroupNotAMemberException extends GroupChangeException {
private final boolean likelyPendingMember;
public GroupNotAMemberException(Throwable throwable) {
super(throwable);
this.likelyPendingMember = false;
}
public GroupNotAMemberException(GroupNotAMemberException throwable, boolean likelyPendingMember) {
super(throwable.getCause() != null ? throwable.getCause() : throwable);
this.likelyPendingMember = likelyPendingMember;
}
GroupNotAMemberException() {
this.likelyPendingMember = false;
}
public boolean isLikelyPendingMember() {
return likelyPendingMember;
}
}

View File

@@ -72,7 +72,7 @@ public final class GroupsV1MigrationUtil {
throw new InvalidMigrationStateException();
}
switch (GroupManager.v2GroupStatus(context, gv2MasterKey)) {
switch (GroupManager.v2GroupStatus(context, SignalStore.account().getAci(), gv2MasterKey)) {
case DOES_NOT_EXIST:
Log.i(TAG, "Group does not exist on the service.");
@@ -172,7 +172,7 @@ public final class GroupsV1MigrationUtil {
try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock()){
DecryptedGroup decryptedGroup;
try {
decryptedGroup = GroupManager.addedGroupVersion(context, gv1Id.deriveV2MigrationMasterKey());
decryptedGroup = GroupManager.addedGroupVersion(SignalStore.account().requireAci(), context, gv1Id.deriveV2MigrationMasterKey());
} catch (GroupDoesNotExistException e) {
throw new IOException("[Local] The group should exist already!");
} catch (GroupNotAMemberException e) {
@@ -186,7 +186,7 @@ public final class GroupsV1MigrationUtil {
Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision());
try {
GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
GroupManager.updateGroupFromServer(context, SignalStore.account().requireAci(), gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
} catch (GroupChangeBusyException | GroupNotAMemberException e) {
Log.w(TAG, e);
}

View File

@@ -6,37 +6,53 @@ import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class GroupsV2Authorization {
private static final String TAG = Log.tag(GroupsV2Authorization.class);
private final ValueCache cache;
private final ValueCache aciCache;
private final ValueCache pniCache;
private final GroupsV2Api groupsV2Api;
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache cache) {
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache aciCache, @NonNull ValueCache pniCache) {
this.groupsV2Api = groupsV2Api;
this.cache = cache;
this.aciCache = aciCache;
this.pniCache = pniCache;
}
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ACI self,
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
@NonNull GroupSecretParams groupSecretParams)
throws IOException, VerificationFailedException
{
boolean isPni = Objects.equals(authServiceId, SignalStore.account().getPni());
ValueCache cache = isPni ? pniCache : aciCache;
return getAuthorizationForToday(authServiceId, cache, groupSecretParams, !isPni);
}
private GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
@NonNull ValueCache cache,
@NonNull GroupSecretParams groupSecretParams,
boolean isAci)
throws IOException, VerificationFailedException
{
final int today = currentTimeDays();
Map<Integer, AuthCredentialResponse> credentials = cache.read();
try {
return getAuthorization(self, groupSecretParams, credentials, today);
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
} catch (NoCredentialForRedemptionTimeException e) {
Log.i(TAG, "Auth out of date, will update auth and try again");
cache.clear();
@@ -46,11 +62,11 @@ public class GroupsV2Authorization {
}
Log.i(TAG, "Getting new auth credential responses");
credentials = groupsV2Api.getCredentials(today);
credentials = groupsV2Api.getCredentials(today, isAci);
cache.write(credentials);
try {
return getAuthorization(self, groupSecretParams, credentials, today);
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
} catch (NoCredentialForRedemptionTimeException e) {
Log.w(TAG, "The credentials returned did not include the day requested");
throw new IOException("Failed to get credentials");
@@ -58,14 +74,15 @@ public class GroupsV2Authorization {
}
public void clear() {
cache.clear();
aciCache.clear();
pniCache.clear();
}
private static int currentTimeDays() {
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
}
private GroupsV2AuthorizationString getAuthorization(ACI self,
private GroupsV2AuthorizationString getAuthorization(ServiceId authServiceId,
GroupSecretParams groupSecretParams,
Map<Integer, AuthCredentialResponse> credentials,
int today)
@@ -77,7 +94,7 @@ public class GroupsV2Authorization {
throw new NoCredentialForRedemptionTimeException();
}
return groupsV2Api.getGroupsV2AuthorizationString(self, today, groupSecretParams, authCredentialResponse);
return groupsV2Api.getGroupsV2AuthorizationString(authServiceId, today, groupSecretParams, authCredentialResponse);
}
public interface ValueCache {

View File

@@ -58,7 +58,8 @@ final class AddGroupDetailsRepository {
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
try {
GroupManager.GroupActionResult result = GroupManager.createGroup(context,
GroupManager.GroupActionResult result = GroupManager.createGroup(SignalStore.account().requireAci(),
context,
recipients,
avatar,
name,

View File

@@ -100,7 +100,7 @@ final class PendingMemberInvitesRepository {
@WorkerThread
boolean revokeInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts) {
try {
GroupManager.revokeInvites(context, groupId, uuidCipherTexts);
GroupManager.revokeInvites(context, SignalStore.account().requireAci(), groupId, uuidCipherTexts);
return true;
} catch (GroupChangeException | IOException e) {
Log.w(TAG, e);

View File

@@ -18,6 +18,7 @@ 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.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.MessageDatabase;
@@ -52,7 +53,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException;
@@ -67,7 +67,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Advances a groups state to a specified revision.
@@ -104,11 +103,10 @@ public class GroupsV2StateProcessor {
this.groupDatabase = SignalDatabase.groups();
}
public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) {
ACI selfAci = SignalStore.account().requireAci();
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, selfAci, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
public StateProcessorForGroup forGroup(@NonNull ServiceId serviceId, @NonNull GroupMasterKey groupMasterKey) {
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceId, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
return new StateProcessorForGroup(selfAci, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
return new StateProcessorForGroup(serviceId, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
}
public enum GroupState {
@@ -142,7 +140,7 @@ public class GroupsV2StateProcessor {
}
public static final class StateProcessorForGroup {
private final ACI selfAci;
private final ServiceId serviceId;
private final Context context;
private final GroupDatabase groupDatabase;
private final GroupsV2Api groupsV2Api;
@@ -152,7 +150,7 @@ public class GroupsV2StateProcessor {
private final GroupSecretParams groupSecretParams;
private final ProfileAndMessageHelper profileAndMessageHelper;
@VisibleForTesting StateProcessorForGroup(@NonNull ACI selfAci,
@VisibleForTesting StateProcessorForGroup(@NonNull ServiceId serviceId,
@NonNull Context context,
@NonNull GroupDatabase groupDatabase,
@NonNull GroupsV2Api groupsV2Api,
@@ -160,7 +158,7 @@ public class GroupsV2StateProcessor {
@NonNull GroupMasterKey groupMasterKey,
@NonNull ProfileAndMessageHelper profileAndMessageHelper)
{
this.selfAci = selfAci;
this.serviceId = serviceId;
this.context = context;
this.groupDatabase = groupDatabase;
this.groupsV2Api = groupsV2Api;
@@ -197,7 +195,7 @@ public class GroupsV2StateProcessor {
revision == signedGroupChange.getRevision())
{
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange)) {
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) {
Log.w(TAG, "Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch.");
} else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
Log.w(TAG, "Ignoring P2P group change by setting");
@@ -233,8 +231,9 @@ public class GroupsV2StateProcessor {
}
if (inputGroupState == null) {
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, selfAci.uuid())) {
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceId.uuid())) {
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member");
throw new GroupNotAMemberException(e, true);
} else {
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
insertGroupLeave();
@@ -277,26 +276,34 @@ public class GroupsV2StateProcessor {
.map(DecryptedMember::getUuid)
.map(UuidUtil::fromByteStringOrNull)
.filter(Objects::nonNull)
.collect(Collectors.toSet())
.contains(selfAci.uuid());
.anyMatch(u -> u.equals(serviceId.uuid()));
boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList()
.stream()
.map(DecryptedPendingMember::getUuid)
.map(UuidUtil::fromByteStringOrNull)
.filter(Objects::nonNull)
.collect(Collectors.toSet())
.contains(selfAci.uuid());
.anyMatch(u -> u.equals(serviceId.uuid()));
return !currentlyInGroup && !addedAsMember && !addedAsPendingMember;
}
private boolean notHavingInviteRevoked(@NonNull DecryptedGroupChange signedGroupChange) {
boolean havingInviteRevoked = signedGroupChange.getDeletePendingMembersList()
.stream()
.map(DecryptedPendingMemberRemoval::getUuid)
.map(UuidUtil::fromByteStringOrNull)
.filter(Objects::nonNull)
.anyMatch(u -> u.equals(serviceId.uuid()));
return !havingInviteRevoked;
}
/**
* Using network, attempt to bring the local copy of the group up to the revision specified via paging.
*/
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException {
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
ACI selfAci = this.selfAci;
Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
@@ -304,37 +311,38 @@ public class GroupsV2StateProcessor {
GlobalGroupState inputGroupState;
try {
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
} catch (NotInGroupException | GroupNotFoundException e) {
throw new GroupNotAMemberException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new IOException(e);
}
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList())) {
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList())) {
Log.i(TAG, "Local state is at or later than server");
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
}
if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) {
if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceId.uuid(), latestServerGroup.getMembersList())) {
Log.i(TAG, "Latest revision or not a member, use latest only");
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null)));
} else {
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid());
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
boolean includeFirstState = forceIncludeFirst ||
localState == null ||
localState.getRevision() < 0 ||
localState.getRevision() == revisionWeWereAdded ||
!GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList()) ||
(revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceId.uuid());
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
boolean includeFirstState = forceIncludeFirst ||
localState == null ||
localState.getRevision() < 0 ||
localState.getRevision() == revisionWeWereAdded ||
!GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList()) ||
(revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
Log.i(TAG,
"Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") +
" logsNeededFrom: " + logsNeededFrom +
" includeFirstState: " + includeFirstState +
" forceIncludeFirst: " + forceIncludeFirst);
inputGroupState = getFullMemberHistoryPage(localState, selfAci, logsNeededFrom, includeFirstState);
inputGroupState = getFullMemberHistoryPage(localState, serviceId, logsNeededFrom, includeFirstState);
}
ProfileKeySet profileKeys = new ProfileKeySet();
@@ -350,7 +358,7 @@ public class GroupsV2StateProcessor {
if (newLocalState != null && !inputGroupState.hasMore() && !forceIncludeFirst) {
int newLocalRevision = newLocalState.getRevision();
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
if (newLocalRevision < requestRevision) {
Log.w(TAG, "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]");
return updateLocalGroupFromServerPaged(revision, localState, timestamp, true);
@@ -382,7 +390,7 @@ public class GroupsV2StateProcessor {
if (hasMore) {
Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
inputGroupState = getFullMemberHistoryPage(finalState, selfAci, inputGroupState.getNextPageRevision(), false);
inputGroupState = getFullMemberHistoryPage(finalState, serviceId, inputGroupState.getNextPageRevision(), false);
}
}
@@ -406,7 +414,7 @@ public class GroupsV2StateProcessor {
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
{
try {
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
} catch (GroupNotFoundException e) {
throw new GroupDoesNotExistException(e);
} catch (NotInGroupException e) {
@@ -421,7 +429,7 @@ public class GroupsV2StateProcessor {
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
{
try {
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), true)
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), true)
.getResults()
.get(0)
.getGroup()
@@ -442,7 +450,7 @@ public class GroupsV2StateProcessor {
}
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
UUID selfUuid = selfAci.uuid();
UUID selfUuid = serviceId.uuid();
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
.requireV2GroupProperties()
@@ -501,7 +509,7 @@ public class GroupsV2StateProcessor {
boolean needsAvatarFetch;
if (inputGroupState.getLocalState() == null) {
groupDatabase.create(masterKey, newLocalState);
groupDatabase.create(serviceId, masterKey, newLocalState);
needsAvatarFetch = !TextUtils.isEmpty(newLocalState.getAvatar());
} else {
groupDatabase.update(masterKey, newLocalState);
@@ -515,9 +523,9 @@ public class GroupsV2StateProcessor {
profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState);
}
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ACI selfAci, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ServiceId serviceId, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
try {
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), includeFirstState);
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), includeFirstState);
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupHistoryPage.getResults().size());
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
@@ -545,14 +553,14 @@ public class GroupsV2StateProcessor {
static class ProfileAndMessageHelper {
private final Context context;
private final ACI selfAci;
private final ServiceId serviceId;
private final GroupMasterKey masterKey;
private final GroupId.V2 groupId;
private final RecipientDatabase recipientDatabase;
ProfileAndMessageHelper(@NonNull Context context, @NonNull ACI selfAci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
this.context = context;
this.selfAci = selfAci;
this.serviceId = serviceId;
this.masterKey = masterKey;
this.groupId = groupId;
this.recipientDatabase = recipientDatabase;
@@ -560,15 +568,15 @@ public class GroupsV2StateProcessor {
void determineProfileSharing(@NonNull GlobalGroupState inputGroupState, @NonNull DecryptedGroup newLocalState) {
if (inputGroupState.getLocalState() != null) {
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), selfAci.uuid()).isPresent();
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), serviceId.uuid()).isPresent();
if (wasAMemberAlready) {
return;
}
}
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), selfAci.uuid());
Optional<DecryptedPendingMember> selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), selfAci.uuid());
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), serviceId.uuid());
Optional<DecryptedPendingMember> selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), serviceId.uuid());
if (selfAsMemberOptional.isPresent()) {
DecryptedMember selfAsMember = selfAsMemberOptional.get();
@@ -587,7 +595,7 @@ public class GroupsV2StateProcessor {
Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId()));
if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), selfAci.uuid()))) {
if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), serviceId.uuid()))) {
Log.i(TAG, "Added by a blocked user. Leaving group.");
ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId));
//noinspection UnnecessaryReturnStatement
@@ -671,7 +679,7 @@ public class GroupsV2StateProcessor {
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
Optional<ServiceId> editor = getEditor(decryptedGroupV2Context).map(ServiceId::from);
boolean outgoing = !editor.isPresent() || selfAci.equals(editor.get());
boolean outgoing = !editor.isPresent() || serviceId.equals(editor.get());
if (outgoing) {
try {
@@ -709,7 +717,7 @@ public class GroupsV2StateProcessor {
if (changeEditor.isPresent()) {
return changeEditor;
} else {
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), selfAci.uuid());
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), serviceId.uuid());
if (pendingByUuid.isPresent()) {
return Optional.ofNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid()));
}

View File

@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -15,8 +16,10 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
@@ -89,7 +92,24 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob {
return;
}
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
ServiceId authServiceId = group.get().getAuthServiceId() != null ? group.get().getAuthServiceId() : SignalStore.account().requireAci();
try {
GroupManager.updateGroupFromServer(context, authServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
} catch (GroupNotAMemberException | IOException e) {
ServiceId otherServiceId = authServiceId.equals(SignalStore.account().getPni()) ? SignalStore.account().getAci() : SignalStore.account().getPni();
boolean isNotAMemberOrPending = e instanceof GroupNotAMemberException && !((GroupNotAMemberException) e).isLikelyPendingMember();
boolean verificationFailed = e.getCause() instanceof VerificationFailedException;
if (otherServiceId != null && (isNotAMemberOrPending || verificationFailed)) {
Log.i(TAG, "Request failed, attempting with other id");
GroupManager.updateGroupFromServer(context, otherServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
Log.i(TAG, "Request succeeded with other credential. Associating " + otherServiceId + " with group " + groupId);
SignalDatabase.groups().setAuthServiceId(otherServiceId, groupId);
} else {
throw e;
}
}
}
@Override

View File

@@ -111,6 +111,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
putString(KEY_PNI, pni.toString())
}
fun requireServiceIds(): ServiceIds {
return ServiceIds(requireAci(), requirePni())
}
/** The local user's E164. */
val e164: String?
get() = getString(KEY_E164, null)

View File

@@ -21,37 +21,49 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class);
private static final String PREFIX = "gv2:auth_token_cache";
private static final int VERSION = 2;
private static final String KEY = PREFIX + ":" + VERSION;
private static final String ACI_PREFIX = "gv2:auth_token_cache";
private static final int ACI_VERSION = 2;
private static final String PNI_PREFIX = "gv2:auth_token_cache:pni";
private static final int PNI_VERSION = 1;
private final String key;
private final KeyValueStore store;
GroupsV2AuthorizationSignalStoreCache(KeyValueStore store) {
this.store = store;
if (store.containsKey(PREFIX)) {
public static GroupsV2AuthorizationSignalStoreCache createAciCache(@NonNull KeyValueStore store) {
if (store.containsKey(ACI_PREFIX)) {
store.beginWrite()
.remove(PREFIX)
.remove(ACI_PREFIX)
.commit();
}
return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PREFIX + ":" + ACI_VERSION);
}
public static GroupsV2AuthorizationSignalStoreCache createPniCache(@NonNull KeyValueStore store) {
return new GroupsV2AuthorizationSignalStoreCache(store, PNI_PREFIX + ":" + PNI_VERSION);
}
private GroupsV2AuthorizationSignalStoreCache(@NonNull KeyValueStore store, @NonNull String key) {
this.store = store;
this.key = key;
}
@Override
public void clear() {
store.beginWrite()
.remove(KEY)
.remove(key)
.commit();
Log.i(TAG, "Cleared local response cache");
info("Cleared local response cache");
}
@Override
public @NonNull Map<Integer, AuthCredentialResponse> read() {
byte[] credentialBlob = store.getBlob(KEY, null);
byte[] credentialBlob = store.getBlob(key, null);
if (credentialBlob == null) {
Log.i(TAG, "No credentials responses are cached locally");
info("No credentials responses are cached locally");
return Collections.emptyMap();
}
@@ -63,7 +75,7 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
result.put(credential.getDate(), new AuthCredentialResponse(credential.getAuthCredentialResponse().toByteArray()));
}
Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
info(String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
return result;
} catch (InvalidProtocolBufferException | InvalidInputException e) {
@@ -82,9 +94,13 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
}
store.beginWrite()
.putBlob(KEY, builder.build().toByteArray())
.putBlob(key, builder.build().toByteArray())
.commit();
Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size()));
info(String.format(Locale.US, "Written %d credentials to local storage", values.size()));
}
private void info(String message) {
Log.i(TAG, (key.startsWith(PNI_PREFIX) ? "[PNI]" : "[ACI]") + " " + message);
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.keyvalue
import com.google.protobuf.ByteString
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
/**
* Helper for dealing with [ServiceId] matching when you only care that either of your
* service ids match but don't care which one.
*/
data class ServiceIds(val aci: ACI, val pni: PNI) {
private val aciByteString: ByteString by lazy { UuidUtil.toByteString(aci.uuid()) }
private val pniByteString: ByteString by lazy { UuidUtil.toByteString(pni.uuid()) }
fun matches(uuid: UUID): Boolean {
return uuid == aci.uuid() || uuid == pni.uuid()
}
fun matches(uuid: ByteString): Boolean {
return uuid == aciByteString || uuid == pniByteString
}
}

View File

@@ -261,8 +261,12 @@ public final class SignalStore {
return getInstance().storyValues;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AciAuthorizationCache() {
return GroupsV2AuthorizationSignalStoreCache.createAciCache(getStore());
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2PniAuthorizationCache() {
return GroupsV2AuthorizationSignalStoreCache.createPniCache(getStore());
}
public static @NonNull PreferenceDataStore getPreferenceDataStore() {

View File

@@ -12,6 +12,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.mobilecoin.lib.exceptions.SerializationException;
import org.signal.core.util.Hex;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
@@ -126,7 +127,6 @@ import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
@@ -168,6 +168,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.payments.Money;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
@@ -532,8 +533,13 @@ public final class MessageContentProcessor {
throws IOException, GroupChangeBusyException
{
try {
long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1;
GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange());
ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid());
if (authServiceId == null) {
warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI");
authServiceId = SignalStore.account().requireAci();
}
long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1;
GroupManager.updateGroupFromServer(context, authServiceId, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange());
return true;
} catch (GroupNotAMemberException e) {
warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in");
@@ -816,7 +822,12 @@ public final class MessageContentProcessor {
}
} else if (group.getGroupV2().isPresent()) {
warn(content.getTimestamp(), "Received a GV2 message for a group we have no knowledge of -- attempting to fix this state.");
SignalDatabase.groups().fixMissingMasterKey(group.getGroupV2().get().getMasterKey());
ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid());
if (authServiceId == null) {
warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI");
authServiceId = SignalStore.account().requireAci();
}
SignalDatabase.groups().fixMissingMasterKey(authServiceId, group.getGroupV2().get().getMasterKey());
} else {
warn(content.getTimestamp(), "Received a message for a group we don't know about without a group context. Ignoring.");
}

View File

@@ -154,7 +154,7 @@ public class RecipientUtil {
recipient = recipient.resolve();
if (recipient.isGroup() && recipient.getGroupId().get().isPush() && recipient.isActiveGroup()) {
if (recipient.isGroup() && recipient.getGroupId().get().isPush()) {
GroupManager.leaveGroupFromBlockOrMessageRequest(context, recipient.getGroupId().get().requirePush());
}

View File

@@ -57,7 +57,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
return StorageSyncModels.localToRemoteRecord(settings);
} else {
Log.w(TAG, "No local master key. Assuming it matches remote since the groupIds match. Enqueuing a fetch to fix the bad state.");
groupDatabase.fixMissingMasterKey(record.getMasterKeyOrThrow());
groupDatabase.fixMissingMasterKey(null, record.getMasterKeyOrThrow());
return StorageSyncModels.localToRemoteRecord(settings, record.getMasterKeyOrThrow());
}
})

View File

@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.AppSignatureUtil
import shark.AndroidReferenceMatchers
import java.util.Locale
class SpinnerApplicationContext : ApplicationContext() {
override fun onCreate() {
@@ -36,7 +37,8 @@ class SpinnerApplicationContext : ApplicationContext() {
"Profile Name" to (if (SignalStore.account().isRegistered) Recipient.self().profileName.toString() else "none"),
"E164" to (SignalStore.account().e164 ?: "none"),
"ACI" to (SignalStore.account().aci?.toString() ?: "none"),
"PNI" to (SignalStore.account().pni?.toString() ?: "none")
"PNI" to (SignalStore.account().pni?.toString() ?: "none"),
Spinner.KEY_ENVIRONMENT to BuildConfig.FLAVOR_environment.toUpperCase(Locale.US)
),
linkedMapOf(
"signal" to DatabaseConfig(

View File

@@ -1,10 +1,16 @@
package org.thoughtcrime.securesms.database
import android.database.Cursor
import com.google.protobuf.ByteString
import org.signal.core.util.requireBlob
import org.signal.core.util.requireString
import org.signal.spinner.ColumnTransformer
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.whispersystems.signalservice.api.util.UuidUtil
object GV2Transformer : ColumnTransformer {
override fun matches(tableName: String?, columnName: String): Boolean {
@@ -24,6 +30,11 @@ object GV2Transformer : ColumnTransformer {
}
private fun DecryptedGroup.formatAsHtml(): String {
val members: String = describeList(membersList, DecryptedMember::getUuid)
val pending: String = describeList(pendingMembersList, DecryptedPendingMember::getUuid)
val requesting: String = describeList(requestingMembersList, DecryptedRequestingMember::getUuid)
val banned: String = describeList(bannedMembersList, DecryptedBannedMember::getUuid)
return """
Revision: $revision
Title: $title
@@ -32,9 +43,24 @@ private fun DecryptedGroup.formatAsHtml(): String {
Description: "$description"
Announcement: $isAnnouncementGroup
Access: attributes(${accessControl.attributes}) members(${accessControl.members}) link(${accessControl.addFromInviteLink})
Members: $membersCount
Pending: $pendingMembersCount
Requesting: $requestingMembersCount
Banned: $bannedMembersCount
Members: $members
Pending: $pending
Requesting: $requesting
Banned: $banned
""".trimIndent().replace("\n", "<br>")
}
private fun <T> describeList(list: List<T>, getUuid: (T) -> ByteString): String {
return if (list.isNotEmpty() && list.size < 10) {
var pendingMembers = "${list.size}\n"
list.forEachIndexed { i, pendingMember ->
pendingMembers += " ${UuidUtil.fromByteString(getUuid(pendingMember))}"
if (i != list.lastIndex) {
pendingMembers += "\n"
}
}
pendingMembers
} else {
list.size.toString()
}
}

View File

@@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
fun DecryptedGroupChange.Builder.setNewDescription(description: String) {
newDescription = DecryptedString.newBuilder().setValue(description).build()
@@ -107,6 +108,7 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou
var requestedRevision: Int = 0
fun localState(
active: Boolean = true,
revision: Int = 0,
title: String = "",
avatar: String = "",
@@ -116,10 +118,11 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou
pendingMembers: List<DecryptedPendingMember> = emptyList(),
requestingMembers: List<DecryptedRequestingMember> = emptyList(),
inviteLinkPassword: ByteArray = ByteArray(0),
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance()
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance(),
serviceId: String = ServiceId.from(UUID.randomUUID()).toString()
) {
localState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer)
groupRecord = groupRecord(masterKey, localState!!)
groupRecord = groupRecord(masterKey, localState!!, active = active, serviceId = serviceId)
}
fun serverState(
@@ -170,7 +173,8 @@ fun groupRecord(
active: Boolean = true,
avatarDigest: ByteArray = ByteArray(0),
mms: Boolean = false,
distributionId: DistributionId? = null
distributionId: DistributionId? = null,
serviceId: String = ServiceId.from(UUID.randomUUID()).toString()
): Optional<GroupDatabase.GroupRecord> {
return Optional.of(
GroupDatabase.GroupRecord(
@@ -189,7 +193,8 @@ fun groupRecord(
masterKey.serialize(),
decryptedGroup.revision,
decryptedGroup.toByteArray(),
distributionId
distributionId,
serviceId
)
)
}

View File

@@ -24,8 +24,11 @@ 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.DecryptedPendingMember;
import org.thoughtcrime.securesms.keyvalue.ServiceIds;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -44,9 +47,9 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.signal.core.util.StringUtil.isolateBidi;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown;
import static org.signal.core.util.StringUtil.isolateBidi;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@@ -83,7 +86,7 @@ public final class GroupsV2UpdateMessageProducerTest {
Recipient aliceRecipient = recipientWithName(aliceId, "Alice");
Recipient bobRecipient = recipientWithName(bobId, "Bob");
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), you, null);
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), new ServiceIds(ACI.from(you), PNI.from(UUID.randomUUID())), null);
recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(alice), null)).thenReturn(aliceId);
recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(bob), null)).thenReturn(bobId);

View File

@@ -39,6 +39,7 @@ 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.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@@ -53,6 +54,7 @@ class GroupManagerV2Test_edit {
val groupId: GroupId.V2 = GroupId.v2(masterKey)
val selfAci: ACI = ACI.from(UUID.randomUUID())
val selfPni: PNI = PNI.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))
@@ -102,6 +104,7 @@ class GroupManagerV2Test_edit {
groupsV2Authorization,
groupsV2StateProcessor,
selfAci,
selfPni,
groupCandidateHelper,
sendGroupUpdateHelper
)
@@ -114,7 +117,7 @@ class GroupManagerV2Test_edit {
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(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any(), Mockito.anyBoolean())
Mockito.doReturn(data.groupChange!!).`when`(groupsV2API).patchGroup(Mockito.any(), Mockito.any(), Mockito.any())
}
@@ -136,7 +139,8 @@ class GroupManagerV2Test_edit {
members = listOf(
member(selfAci, role = Member.Role.ADMINISTRATOR),
member(otherSid)
)
),
serviceId = selfAci.toString()
)
groupChange(6) {
source(selfAci)

View File

@@ -51,11 +51,11 @@ import java.util.UUID
class GroupsV2StateProcessorTest {
companion object {
val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
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 val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
private val selfAci: ACI = ACI.from(UUID.randomUUID())
private val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
private val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
private val others: List<DecryptedMember> = listOf(member(otherSid))
}
private lateinit var groupDatabase: GroupDatabase
@@ -89,8 +89,7 @@ class GroupsV2StateProcessorTest {
}
private fun given(init: GroupStateTestData.() -> Unit) {
val data = GroupStateTestData(masterKey)
data.init()
val data = givenData(init)
doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java))
doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any())
@@ -109,6 +108,12 @@ class GroupsV2StateProcessorTest {
}
}
private fun givenData(init: GroupStateTestData.() -> Unit): GroupStateTestData {
val data = GroupStateTestData(masterKey)
data.init()
return data
}
@Test
fun `when local revision matches server revision, then return consistent or ahead`() {
given {

View File

@@ -5,6 +5,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
@@ -62,3 +63,9 @@ fun requestingMember(serviceId: ServiceId): DecryptedRequestingMember {
.setUuid(serviceId.toByteString())
.build()
}
fun pendingMember(serviceId: ServiceId): DecryptedPendingMember {
return DecryptedPendingMember.newBuilder()
.setUuid(serviceId.toByteString())
.build()
}

View File

@@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
import java.util.ArrayList;
@@ -37,6 +38,13 @@ public final class GroupCandidate {
return profileKeyCredential;
}
public ProfileKeyCredential requireProfileKeyCredential() {
if (profileKeyCredential.isPresent()) {
return profileKeyCredential.get();
}
throw new IllegalStateException("no profile key credential");
}
public boolean hasProfileKeyCredential() {
return profileKeyCredential.isPresent();
}

View File

@@ -20,7 +20,7 @@ import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
@@ -44,16 +44,16 @@ public class GroupsV2Api {
/**
* Provides 7 days of credentials, which you should cache.
*/
public HashMap<Integer, AuthCredentialResponse> getCredentials(int today)
public HashMap<Integer, AuthCredentialResponse> getCredentials(int today, boolean isAci)
throws IOException
{
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today));
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today, isAci));
}
/**
* Create an auth token from a credential response.
*/
public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ACI self,
public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ServiceId self,
int today,
GroupSecretParams groupSecretParams,
AuthCredentialResponse authCredentialResponse)

View File

@@ -76,6 +76,7 @@ public final class SignalServiceContent {
private final SignalServiceContentProto serializedState;
private final String serverUuid;
private final Optional<byte[]> groupId;
private final String destinationUuid;
private final Optional<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
@@ -96,6 +97,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -106,6 +108,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.ofNullable(message);
@@ -128,6 +131,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -138,6 +142,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -160,6 +165,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -170,6 +176,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -192,6 +199,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -202,6 +210,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -224,6 +233,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -234,6 +244,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -256,6 +267,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -266,6 +278,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -287,6 +300,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -297,6 +311,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -318,6 +333,7 @@ public final class SignalServiceContent {
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
@@ -328,6 +344,7 @@ public final class SignalServiceContent {
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
@@ -404,6 +421,10 @@ public final class SignalServiceContent {
return groupId;
}
public String getDestinationUuid() {
return destinationUuid;
}
public byte[] serialize() {
return serializedState.toByteArray();
}
@@ -443,6 +464,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) {
SignalServiceProtos.Content message = serviceContentProto.getContent();
@@ -467,6 +489,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) {
return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()),
@@ -479,6 +502,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasCallMessage()) {
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
@@ -491,6 +515,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasReceiptMessage()) {
return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()),
@@ -503,6 +528,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasTypingMessage()) {
return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()),
@@ -515,6 +541,7 @@ public final class SignalServiceContent {
false,
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasDecryptionErrorMessage()) {
return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()),
@@ -527,6 +554,7 @@ public final class SignalServiceContent {
metadata.isNeedsReceipt(),
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (senderKeyDistributionMessage.isPresent()) {
return new SignalServiceContent(senderKeyDistributionMessage.get(),
@@ -538,6 +566,7 @@ public final class SignalServiceContent {
false,
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasStoryMessage()) {
return new SignalServiceContent(createStoryMessage(message.getStoryMessage()),
@@ -549,6 +578,7 @@ public final class SignalServiceContent {
false,
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
}
}

View File

@@ -36,23 +36,4 @@ public final class ACI extends ServiceId {
public byte[] toByteArray() {
return UuidUtil.toByteArray(uuid);
}
@Override
public boolean equals(Object other) {
if (other instanceof ServiceId) {
return uuid.equals(((ServiceId) other).uuid);
} else {
return false;
}
}
@Override
public int hashCode() {
return uuid.hashCode();
}
@Override
public String toString() {
return uuid.toString();
}
}

View File

@@ -26,23 +26,4 @@ public final class PNI extends ServiceId {
private PNI(UUID uuid) {
super(uuid);
}
@Override
public int hashCode() {
return uuid.hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof PNI) {
return uuid.equals(((PNI) other).uuid);
} else {
return false;
}
}
@Override
public String toString() {
return uuid.toString();
}
}

View File

@@ -230,7 +230,7 @@ public class PushServiceSocket {
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
private static final String STICKER_PATH = "stickers/%s/full/%d";
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d?identity=%s";
private static final String GROUPSV2_GROUP = "/v1/groups/";
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
@@ -2310,11 +2310,11 @@ public class PushServiceSocket {
public enum ClientSet { ContactDiscovery, KeyBackup }
public CredentialResponse retrieveGroupsV2Credentials(int today)
public CredentialResponse retrieveGroupsV2Credentials(int today, boolean isAci)
throws IOException
{
int todayPlus7 = today + 7;
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7),
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7, isAci ? "aci" : "pni"),
"GET",
null,
NO_HEADERS,

View File

@@ -1,4 +1,4 @@
<h1>SPINNER</h1>
<h1>SPINNER - {{environment}}</h1>
<table class="device-info">
{{#each deviceInfo}}

View File

@@ -11,7 +11,10 @@ import java.io.IOException
* A class to help initialize Spinner, our database debugging interface.
*/
object Spinner {
val TAG: String = Log.tag(Spinner::class.java)
internal const val KEY_PREFIX = "spinner"
const val KEY_ENVIRONMENT = "$KEY_PREFIX:environment"
private val TAG: String = Log.tag(Spinner::class.java)
private lateinit var server: SpinnerServer

View File

@@ -28,7 +28,7 @@ import kotlin.math.min
*/
internal class SpinnerServer(
private val application: Application,
private val deviceInfo: Map<String, String>,
deviceInfo: Map<String, String>,
private val databases: Map<String, DatabaseConfig>
) : NanoHTTPD(5000) {
@@ -36,6 +36,9 @@ internal class SpinnerServer(
private val TAG = Log.tag(SpinnerServer::class.java)
}
private val deviceInfo: Map<String, String> = deviceInfo.filterKeys { !it.startsWith(Spinner.KEY_PREFIX) }
private val environment: String = deviceInfo[Spinner.KEY_ENVIRONMENT] ?: "UNKNOWN"
private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(application)).apply {
registerHelper("eq", ConditionalHelpers.eq)
registerHelper("neq", ConditionalHelpers.neq)
@@ -86,6 +89,7 @@ internal class SpinnerServer(
return renderTemplate(
"overview",
OverviewPageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -101,6 +105,7 @@ internal class SpinnerServer(
return renderTemplate(
"browse",
BrowsePageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -131,6 +136,7 @@ internal class SpinnerServer(
return renderTemplate(
"browse",
BrowsePageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -153,6 +159,7 @@ internal class SpinnerServer(
return renderTemplate(
"query",
QueryPageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -173,6 +180,7 @@ internal class SpinnerServer(
return renderTemplate(
"recent",
RecentPageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -190,6 +198,7 @@ internal class SpinnerServer(
return renderTemplate(
"query",
QueryPageModel(
environment = environment,
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
@@ -343,40 +352,51 @@ internal class SpinnerServer(
return params[name]
}
interface PrefixPageData {
val environment: String
val deviceInfo: Map<String, String>
val database: String
val databases: List<String>
}
data class OverviewPageModel(
val deviceInfo: Map<String, String>,
val database: String,
val databases: List<String>,
override val environment: String,
override val deviceInfo: Map<String, String>,
override val database: String,
override val databases: List<String>,
val tables: List<TableInfo>,
val indices: List<IndexInfo>,
val triggers: List<TriggerInfo>,
val queryResult: QueryResult? = null
)
) : PrefixPageData
data class BrowsePageModel(
val deviceInfo: Map<String, String>,
val database: String,
val databases: List<String>,
override val environment: String,
override val deviceInfo: Map<String, String>,
override val database: String,
override val databases: List<String>,
val tableNames: List<String>,
val table: String? = null,
val queryResult: QueryResult? = null,
val pagingData: PagingData? = null,
)
) : PrefixPageData
data class QueryPageModel(
val deviceInfo: Map<String, String>,
val database: String,
val databases: List<String>,
override val environment: String,
override val deviceInfo: Map<String, String>,
override val database: String,
override val databases: List<String>,
val query: String = "",
val queryResult: QueryResult? = null
)
) : PrefixPageData
data class RecentPageModel(
val deviceInfo: Map<String, String>,
val database: String,
val databases: List<String>,
override val environment: String,
override val deviceInfo: Map<String, String>,
override val database: String,
override val databases: List<String>,
val recentSql: List<RecentQuery>?
)
) : PrefixPageData
data class QueryResult(
val columns: List<String>,