kopia lustrzana https://github.com/ryukoposting/Signal-Android
GV2 Group Manager.
rodzic
ff28d72db6
commit
48a693793f
|
@ -295,7 +295,7 @@ dependencies {
|
|||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation 'org.signal:zkgroup-android:0.4.1'
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
|
|
|
@ -148,9 +148,11 @@ import org.thoughtcrime.securesms.database.model.StickerRecord;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||
|
@ -947,7 +949,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
return ConversationActivity.this.getString(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
} catch (GroupNotAMemberException e) {
|
||||
Log.w(TAG, e);
|
||||
return ConversationActivity.this.getString(R.string.ManageGroupActivity_youre_not_a_member_of_the_group);
|
||||
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return ConversationActivity.this.getString(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||
}
|
||||
|
|
|
@ -267,13 +267,14 @@ public final class GroupDatabase extends Database {
|
|||
create(groupId, null, members, null, null, null, null);
|
||||
}
|
||||
|
||||
public void create(@NonNull GroupId.V2 groupId,
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@Nullable String relay,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull DecryptedGroup groupState)
|
||||
public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull DecryptedGroup groupState)
|
||||
{
|
||||
create(groupId, groupState.getTitle(), Collections.emptyList(), avatar, relay, groupMasterKey, groupState);
|
||||
GroupId.V2 groupId = GroupId.v2(groupMasterKey);
|
||||
|
||||
create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
|
||||
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -287,12 +288,14 @@ public final class GroupDatabase extends Database {
|
|||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@Nullable DecryptedGroup groupState)
|
||||
{
|
||||
List<RecipientId> members = new ArrayList<>(new HashSet<>(memberCollection));
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
|
||||
List<RecipientId> members = new ArrayList<>(new HashSet<>(memberCollection));
|
||||
|
||||
Collections.sort(members);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(RECIPIENT_ID, DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId).serialize());
|
||||
contentValues.put(RECIPIENT_ID, groupRecipientId.serialize());
|
||||
contentValues.put(GROUP_ID, groupId.toString());
|
||||
contentValues.put(TITLE, title);
|
||||
contentValues.put(MEMBERS, RecipientId.toSerializedList(members));
|
||||
|
@ -328,8 +331,11 @@ public final class GroupDatabase extends Database {
|
|||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
if (groupState != null && groupState.hasDisappearingMessagesTimer()) {
|
||||
recipientDatabase.setExpireMessages(groupRecipientId, groupState.getDisappearingMessagesTimer().getDuration());
|
||||
}
|
||||
|
||||
Recipient.live(groupRecipientId).refresh();
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
@ -365,8 +371,10 @@ public final class GroupDatabase extends Database {
|
|||
}
|
||||
|
||||
public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) {
|
||||
String title = decryptedGroup.getTitle();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
|
||||
String title = decryptedGroup.getTitle();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
contentValues.put(TITLE, title);
|
||||
contentValues.put(V2_REVISION, decryptedGroup.getVersion());
|
||||
|
@ -377,8 +385,11 @@ public final class GroupDatabase extends Database {
|
|||
GROUP_ID + " = ?",
|
||||
new String[]{ groupId.toString() });
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
if (decryptedGroup.hasDisappearingMessagesTimer()) {
|
||||
recipientDatabase.setExpireMessages(groupRecipientId, decryptedGroup.getDisappearingMessagesTimer().getDuration());
|
||||
}
|
||||
|
||||
Recipient.live(groupRecipientId).refresh();
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
@ -499,6 +510,21 @@ public final class GroupDatabase extends Database {
|
|||
return RecipientId.toSerializedList(groupMembers);
|
||||
}
|
||||
|
||||
public List<GroupId.V2> getAllGroupV2Ids() {
|
||||
List<GroupId.V2> result = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{ GROUP_ID }, null, null, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
GroupId groupId = GroupId.parseOrThrow(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)));
|
||||
if (groupId.isV2()) {
|
||||
result.add(groupId.requireV2());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
@ -750,8 +776,8 @@ public final class GroupDatabase extends Database {
|
|||
FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true),
|
||||
FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true);
|
||||
|
||||
private boolean includeSelf;
|
||||
private boolean includePending;
|
||||
private final boolean includeSelf;
|
||||
private final boolean includePending;
|
||||
|
||||
MemberSet(boolean includeSelf, boolean includePending) {
|
||||
this.includeSelf = includeSelf;
|
||||
|
|
|
@ -17,10 +17,12 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
|
@ -47,11 +49,14 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
|||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
@ -977,6 +982,30 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the profile key iff currently null.
|
||||
* <p>
|
||||
* If it sets it, it also clears out the profile key credential and resets the unidentified access mode.
|
||||
* @return true iff changed.
|
||||
*/
|
||||
public boolean setProfileKeyIfAbsent(@NonNull RecipientId id, @NonNull ProfileKey profileKey) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String selection = ID + " = ? AND " + PROFILE_KEY + " is NULL";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
ContentValues valuesToSet = new ContentValues(3);
|
||||
|
||||
valuesToSet.put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize()));
|
||||
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
|
||||
valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode());
|
||||
|
||||
if (database.update(TABLE_NAME, valuesToSet, selection, args) > 0) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -1013,6 +1042,56 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in gaps (nulls) in profile key knowledge from new profile keys.
|
||||
* <p>
|
||||
* If from authoritative source, this will overwrite local, otherwise it will only write to the
|
||||
* database if missing.
|
||||
*/
|
||||
public Collection<RecipientId> persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) {
|
||||
Map<UUID, ProfileKey> profileKeys = profileKeySet.getProfileKeys();
|
||||
Map<UUID, ProfileKey> authoritativeProfileKeys = profileKeySet.getAuthoritativeProfileKeys();
|
||||
int totalKeys = profileKeys.size() + authoritativeProfileKeys.size();
|
||||
|
||||
if (totalKeys == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Persisting %d Profile keys, %d of which are authoritative", totalKeys, authoritativeProfileKeys.size()));
|
||||
|
||||
HashSet<RecipientId> updated = new HashSet<>(totalKeys);
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
for (Map.Entry<UUID, ProfileKey> entry : profileKeys.entrySet()) {
|
||||
RecipientId recipientId = getOrInsertFromUuid(entry.getKey());
|
||||
|
||||
if (setProfileKeyIfAbsent(recipientId, entry.getValue())) {
|
||||
Log.i(TAG, "Learned new profile key");
|
||||
updated.add(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<UUID, ProfileKey> entry : authoritativeProfileKeys.entrySet()) {
|
||||
RecipientId recipientId = getOrInsertFromUuid(entry.getKey());
|
||||
|
||||
if (selfId.equals(recipientId)) {
|
||||
Log.i(TAG, "Seen authoritative update for self");
|
||||
if (!entry.getValue().equals(ProfileKeyUtil.getSelfProfileKey())) {
|
||||
Log.w(TAG, "Seen authoritative update for self that didn't match local, scheduling storage sync");
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, String.format("Profile key from owner %s", recipientId));
|
||||
if (setProfileKey(recipientId, entry.getValue())) {
|
||||
Log.i(TAG, "Learned new profile key from owner");
|
||||
updated.add(recipientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
|
|
|
@ -210,23 +210,24 @@ final class GroupsV2UpdateMessageProducer {
|
|||
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(youUuid);
|
||||
|
||||
for (ByteString member : change.getPromotePendingMembersList()) {
|
||||
boolean newMemberIsYou = member.equals(youUuid);
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(youUuid);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_accepted_invite));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_invited_member_s, describe(member)));
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_invited_member_s, describe(uuid)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
|
||||
} else {
|
||||
if (member.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_accepted_invite, describe(member)));
|
||||
if (uuid.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_accepted_invite, describe(uuid)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_invited_member_s, describe(change.getEditor()), describe(member)));
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_invited_member_s, describe(change.getEditor()), describe(uuid)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -282,7 +283,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(youUuid);
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ import androidx.annotation.NonNull;
|
|||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
@ -22,6 +24,8 @@ import org.whispersystems.signalservice.api.KeyBackupService;
|
|||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
|
||||
/**
|
||||
|
@ -37,18 +41,20 @@ public class ApplicationDependencies {
|
|||
private static Application application;
|
||||
private static Provider provider;
|
||||
|
||||
private static SignalServiceAccountManager accountManager;
|
||||
private static SignalServiceMessageSender messageSender;
|
||||
private static SignalServiceMessageReceiver messageReceiver;
|
||||
private static IncomingMessageProcessor incomingMessageProcessor;
|
||||
private static MessageRetriever messageRetriever;
|
||||
private static LiveRecipientCache recipientCache;
|
||||
private static JobManager jobManager;
|
||||
private static FrameRateTracker frameRateTracker;
|
||||
private static KeyValueStore keyValueStore;
|
||||
private static MegaphoneRepository megaphoneRepository;
|
||||
private static GroupsV2Operations groupsV2Operations;
|
||||
private static EarlyMessageCache earlyMessageCache;
|
||||
private static SignalServiceAccountManager accountManager;
|
||||
private static SignalServiceMessageSender messageSender;
|
||||
private static SignalServiceMessageReceiver messageReceiver;
|
||||
private static IncomingMessageProcessor incomingMessageProcessor;
|
||||
private static MessageRetriever messageRetriever;
|
||||
private static LiveRecipientCache recipientCache;
|
||||
private static JobManager jobManager;
|
||||
private static FrameRateTracker frameRateTracker;
|
||||
private static KeyValueStore keyValueStore;
|
||||
private static MegaphoneRepository megaphoneRepository;
|
||||
private static GroupsV2Authorization groupsV2Authorization;
|
||||
private static GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private static GroupsV2Operations groupsV2Operations;
|
||||
private static EarlyMessageCache earlyMessageCache;
|
||||
|
||||
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
|
||||
|
@ -74,6 +80,16 @@ public class ApplicationDependencies {
|
|||
return accountManager;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull GroupsV2Authorization getGroupsV2Authorization() {
|
||||
assertInitialization();
|
||||
|
||||
if (groupsV2Authorization == null) {
|
||||
groupsV2Authorization = getSignalServiceAccountManager().createGroupsV2Authorization(Recipient.self().getUuid().get());
|
||||
}
|
||||
|
||||
return groupsV2Authorization;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull GroupsV2Operations getGroupsV2Operations() {
|
||||
assertInitialization();
|
||||
|
||||
|
@ -91,6 +107,16 @@ public class ApplicationDependencies {
|
|||
10);
|
||||
}
|
||||
|
||||
public static synchronized @NonNull GroupsV2StateProcessor getGroupsV2StateProcessor() {
|
||||
assertInitialization();
|
||||
|
||||
if (groupsV2StateProcessor == null) {
|
||||
groupsV2StateProcessor = new GroupsV2StateProcessor(application);
|
||||
}
|
||||
|
||||
return groupsV2StateProcessor;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull SignalServiceMessageSender getSignalServiceMessageSender() {
|
||||
assertInitialization();
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class BadGroupIdException extends Exception {
|
||||
BadGroupIdException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
BadGroupIdException() {
|
||||
super();
|
||||
}
|
||||
|
||||
BadGroupIdException(Exception e) {
|
||||
BadGroupIdException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
BadGroupIdException(@NonNull Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class GroupChangeBusyException extends Exception {
|
||||
|
||||
public GroupChangeBusyException(@NonNull Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public GroupChangeBusyException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,17 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class GroupChangeFailedException extends Exception {
|
||||
|
||||
GroupChangeFailedException(Throwable throwable) {
|
||||
GroupChangeFailedException() {
|
||||
}
|
||||
|
||||
GroupChangeFailedException(@NonNull Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
GroupChangeFailedException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -34,7 +33,7 @@ public final class GroupManager {
|
|||
{
|
||||
Set<RecipientId> addresses = getMemberIds(members);
|
||||
|
||||
return V1GroupManager.createGroup(context, addresses, avatar, name, mms);
|
||||
return GroupManagerV1.createGroup(context, addresses, avatar, name, mms);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -48,7 +47,7 @@ public final class GroupManager {
|
|||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
return V1GroupManager.updateGroup(context, groupId, getMemberIds(members), avatar, name);
|
||||
return GroupManagerV1.updateGroup(context, groupId, getMemberIds(members), avatar, name);
|
||||
}
|
||||
|
||||
public static GroupActionResult updateGroup(@NonNull Context context,
|
||||
|
@ -60,7 +59,7 @@ public final class GroupManager {
|
|||
{
|
||||
Set<RecipientId> addresses = getMemberIds(members);
|
||||
|
||||
return V1GroupManager.updateGroup(context, groupId, addresses, BitmapUtil.toByteArray(avatar), name);
|
||||
return GroupManagerV1.updateGroup(context, groupId, addresses, BitmapUtil.toByteArray(avatar), name);
|
||||
}
|
||||
|
||||
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
|
||||
|
@ -74,17 +73,29 @@ public final class GroupManager {
|
|||
|
||||
@WorkerThread
|
||||
public static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId) {
|
||||
return V1GroupManager.leaveGroup(context, groupId.requireV1());
|
||||
return GroupManagerV1.leaveGroup(context, groupId.requireV1());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void updateGroupFromServer(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
int version)
|
||||
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId)) {
|
||||
edit.updateLocalToServerVersion(version);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
if (groupId.isV2()) {
|
||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow timer change
|
||||
new GroupManagerV2(context).edit(groupId.requireV2())
|
||||
.updateGroupTimer(expirationTime);
|
||||
} else {
|
||||
V1GroupManager.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
||||
GroupManagerV1.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,9 +45,9 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class V1GroupManager {
|
||||
final class GroupManagerV1 {
|
||||
|
||||
private static final String TAG = Log.tag(V1GroupManager.class);
|
||||
private static final String TAG = Log.tag(GroupManagerV1.class);
|
||||
|
||||
static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
||||
@NonNull Set<RecipientId> memberIds,
|
|
@ -0,0 +1,200 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
final class GroupManagerV2 {
|
||||
|
||||
private static final String TAG = Log.tag(GroupManagerV2.class);
|
||||
|
||||
private final Context context;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
private final GroupsV2Operations groupsV2Operations;
|
||||
private final GroupsV2Authorization authorization;
|
||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private final UUID selfUuid;
|
||||
|
||||
GroupManagerV2(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
|
||||
this.groupsV2Operations = ApplicationDependencies.getGroupsV2Operations();
|
||||
this.authorization = ApplicationDependencies.getGroupsV2Authorization();
|
||||
this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor();
|
||||
this.selfUuid = Recipient.self().getUuid().get();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
GroupEditor edit(@NonNull GroupId.V2 groupId) throws GroupChangeBusyException {
|
||||
return new GroupEditor(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
|
||||
}
|
||||
|
||||
class GroupEditor implements Closeable {
|
||||
|
||||
private final Closeable lock;
|
||||
private final GroupId.V2 groupId;
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
private final GroupsV2Operations.GroupOperations groupOperations;
|
||||
|
||||
GroupEditor(@NonNull GroupId.V2 groupId, @NonNull Closeable lock) {
|
||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
|
||||
|
||||
this.lock = lock;
|
||||
this.groupId = groupId;
|
||||
this.groupMasterKey = v2GroupProperties.getGroupMasterKey();
|
||||
this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
this.groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime));
|
||||
}
|
||||
|
||||
void updateLocalToServerVersion(int version)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
.updateLocalGroupToRevision(version, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private GroupManager.GroupActionResult commitChangeWithConflictResolution(GroupChange.Actions.Builder change)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
change.setSourceUuid(UuidUtil.toByteString(Recipient.self().getUuid().get()));
|
||||
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
return commitChange(change);
|
||||
} catch (ConflictException e) {
|
||||
Log.w(TAG, "Conflict on group");
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
|
||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis());
|
||||
|
||||
if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED || groupUpdateResult.getLatestServer() == null) {
|
||||
throw new GroupChangeFailedException();
|
||||
}
|
||||
|
||||
Log.w(TAG, "Group has been updated");
|
||||
try {
|
||||
change = GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
|
||||
groupOperations.decryptChange(change.build(), selfUuid),
|
||||
change.build());
|
||||
} catch (VerificationFailedException | InvalidGroupStateException ex) {
|
||||
throw new GroupChangeFailedException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
|
||||
}
|
||||
|
||||
private GroupManager.GroupActionResult commitChange(GroupChange.Actions.Builder change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
|
||||
final int nextRevision = v2GroupProperties.getGroupRevision() + 1;
|
||||
final GroupChange.Actions changeActions = change.setVersion(nextRevision).build();
|
||||
final DecryptedGroupChange decryptedChange;
|
||||
final DecryptedGroup decryptedGroupState;
|
||||
|
||||
try {
|
||||
decryptedChange = groupOperations.decryptChange(changeActions, selfUuid);
|
||||
decryptedGroupState = DecryptedGroupUtil.apply(v2GroupProperties.getDecryptedGroup(), decryptedChange);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | DecryptedGroupUtil.NotAbleToApplyChangeException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
commitToServer(changeActions);
|
||||
groupDatabase.update(groupId, decryptedGroupState);
|
||||
|
||||
return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange);
|
||||
}
|
||||
|
||||
private void commitToServer(GroupChange.Actions change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
groupsV2Api.patchGroup(change, groupSecretParams, authorization);
|
||||
} catch (NotInGroupException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupNotAMemberException(e);
|
||||
} catch (AuthorizationFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupInsufficientRightsException(e);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupChangeFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
||||
@NonNull DecryptedGroup decryptedGroup,
|
||||
@Nullable DecryptedGroupChange plainGroupChange)
|
||||
{
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient,
|
||||
decryptedGroupV2Context,
|
||||
null,
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupManager.GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
lock.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
public final class GroupNotAMemberException extends Exception {
|
||||
|
||||
public GroupNotAMemberException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
GroupNotAMemberException() {
|
||||
}
|
||||
}
|
|
@ -3,15 +3,24 @@ package org.thoughtcrime.securesms.groups;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GroupProtoUtil {
|
||||
|
@ -19,6 +28,45 @@ public final class GroupProtoUtil {
|
|||
private GroupProtoUtil() {
|
||||
}
|
||||
|
||||
public static int findVersionWeWereAdded(@NonNull DecryptedGroup group, @NonNull UUID uuid)
|
||||
throws GroupNotAMemberException
|
||||
{
|
||||
ByteString bytes = UuidUtil.toByteString(uuid);
|
||||
for (DecryptedMember decryptedMember : group.getMembersList()) {
|
||||
if (decryptedMember.getUuid().equals(bytes)) {
|
||||
return decryptedMember.getJoinedAtVersion();
|
||||
}
|
||||
}
|
||||
for (DecryptedPendingMember decryptedMember : group.getPendingMembersList()) {
|
||||
if (decryptedMember.getUuid().equals(bytes)) {
|
||||
// Assume latest, we don't have any information about when pending members were invited
|
||||
return group.getVersion();
|
||||
}
|
||||
}
|
||||
throw new GroupNotAMemberException();
|
||||
}
|
||||
|
||||
public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
|
||||
@NonNull DecryptedGroup decryptedGroup,
|
||||
@Nullable DecryptedGroupChange plainGroupChange)
|
||||
{
|
||||
int version = plainGroupChange != null ? plainGroupChange.getVersion() : decryptedGroup.getVersion();
|
||||
SignalServiceProtos.GroupContextV2 groupContext = SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(masterKey.serialize()))
|
||||
.setRevision(version)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context.Builder builder = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(groupContext)
|
||||
.setGroupState(decryptedGroup);
|
||||
|
||||
if (plainGroupChange != null) {
|
||||
builder.setChange(plainGroupChange);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Recipient pendingMemberToRecipient(@NonNull Context context, @NonNull DecryptedPendingMember pendingMember) {
|
||||
return uuidByteStringToRecipient(context, pendingMember.getUuid());
|
||||
|
@ -34,4 +82,16 @@ public final class GroupProtoUtil {
|
|||
|
||||
return Recipient.externalPush(context, uuid, null);
|
||||
}
|
||||
|
||||
public static boolean isMember(@NonNull UUID uuid, @NonNull List<DecryptedMember> membersList) {
|
||||
ByteString uuidBytes = UuidUtil.toByteString(uuid);
|
||||
|
||||
for (DecryptedMember member : membersList) {
|
||||
if (uuidBytes.equals(member.getUuid())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
final class GroupsV2ProcessingLock {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2ProcessingLock.class);
|
||||
|
||||
private GroupsV2ProcessingLock() {
|
||||
}
|
||||
|
||||
private static final Lock lock = new ReentrantLock();
|
||||
|
||||
@WorkerThread
|
||||
static Closeable acquireGroupProcessingLock() throws GroupChangeBusyException {
|
||||
return acquireGroupProcessingLock(5000);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
static Closeable acquireGroupProcessingLock(long timeoutMs) throws GroupChangeBusyException {
|
||||
Util.assertNotMainThread();
|
||||
|
||||
try {
|
||||
if (!lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
|
||||
throw new GroupChangeBusyException("Failed to get a lock on the group processing in the timeout period");
|
||||
}
|
||||
return lock::unlock;
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupChangeBusyException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,16 +11,19 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
final class ManageGroupRepository {
|
||||
|
@ -61,7 +64,10 @@ final class ManageGroupRepository {
|
|||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.NO_RIGHTS);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
} catch (GroupNotAMemberException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.NOT_A_MEMBER);
|
||||
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.OTHER);
|
||||
}
|
||||
|
@ -132,6 +138,7 @@ final class ManageGroupRepository {
|
|||
|
||||
public enum FailureReason {
|
||||
NO_RIGHTS(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this),
|
||||
NOT_A_MEMBER(R.string.ManageGroupActivity_youre_not_a_member_of_the_group),
|
||||
OTHER(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||
|
||||
private final @StringRes int toastMessage;
|
||||
|
|
|
@ -24,17 +24,17 @@ public final class GroupRightsDialog {
|
|||
rights = currentRights;
|
||||
|
||||
builder = new AlertDialog.Builder(context)
|
||||
.setTitle(type.message)
|
||||
.setSingleChoiceItems(type.choices, currentRights.ordinal(), (dialog, which) -> rights = GroupAccessControl.values()[which])
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
GroupAccessControl newGroupAccessControl = rights;
|
||||
.setTitle(type.message)
|
||||
.setSingleChoiceItems(type.choices, currentRights.ordinal(), (dialog, which) -> rights = GroupAccessControl.values()[which])
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
GroupAccessControl newGroupAccessControl = rights;
|
||||
|
||||
if (newGroupAccessControl != currentRights) {
|
||||
onChange.changed(currentRights, newGroupAccessControl);
|
||||
}
|
||||
});
|
||||
if (newGroupAccessControl != currentRights) {
|
||||
onChange.changed(currentRights, newGroupAccessControl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void show() {
|
||||
|
@ -46,6 +46,7 @@ public final class GroupRightsDialog {
|
|||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
MEMBERSHIP(R.string.GroupManagement_choose_who_can_add_or_invite_new_members,
|
||||
R.array.GroupManagement_edit_group_membership_choices),
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Collects profile keys from group states.
|
||||
* <p>
|
||||
* Separates out "authoritative" profile keys that came from a group update created by their owner.
|
||||
* <p>
|
||||
* Authoritative profile keys can be used to overwrite local profile keys.
|
||||
* Non-authoritative profile keys can be used to fill in missing knowledge.
|
||||
*/
|
||||
public final class ProfileKeySet {
|
||||
|
||||
private static final String TAG = Log.tag(ProfileKeySet.class);
|
||||
|
||||
private final Map<UUID, ProfileKey> profileKeys = new LinkedHashMap<>();
|
||||
private final Map<UUID, ProfileKey> authoritativeProfileKeys = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Add new profile keys from the group state.
|
||||
*/
|
||||
public void addKeysFromGroupState(@NonNull DecryptedGroup group,
|
||||
@Nullable UUID changeSource)
|
||||
{
|
||||
for (DecryptedMember member : group.getMembersList()) {
|
||||
UUID memberUuid = UuidUtil.fromByteString(member.getUuid());
|
||||
ProfileKey profileKey;
|
||||
try {
|
||||
profileKey = new ProfileKey(member.getProfileKey().toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Bad profile key in group");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (changeSource != null) {
|
||||
Log.d(TAG, String.format("Change %s by %s", memberUuid, changeSource));
|
||||
|
||||
if (changeSource.equals(memberUuid)) {
|
||||
authoritativeProfileKeys.put(memberUuid, profileKey);
|
||||
profileKeys.remove(memberUuid);
|
||||
} else {
|
||||
if (!authoritativeProfileKeys.containsKey(memberUuid)) {
|
||||
profileKeys.put(memberUuid, profileKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<UUID, ProfileKey> getProfileKeys() {
|
||||
return profileKeys;
|
||||
}
|
||||
|
||||
public Map<UUID, ProfileKey> getAuthoritativeProfileKeys() {
|
||||
return authoritativeProfileKeys;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Advances a groups state to a specified revision.
|
||||
*/
|
||||
public final class GroupsV2StateProcessor {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2StateProcessor.class);
|
||||
|
||||
public static final int LATEST = GroupStateMapper.LATEST;
|
||||
|
||||
private final Context context;
|
||||
private final JobManager jobManager;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Authorization groupsV2Authorization;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
|
||||
public GroupsV2StateProcessor(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.jobManager = ApplicationDependencies.getJobManager();
|
||||
this.groupsV2Authorization = ApplicationDependencies.getGroupsV2Authorization();
|
||||
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
|
||||
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
}
|
||||
|
||||
public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) {
|
||||
return new StateProcessorForGroup(groupMasterKey);
|
||||
}
|
||||
|
||||
public enum GroupState {
|
||||
/**
|
||||
* The message revision was inconsistent with server revision, should ignore
|
||||
*/
|
||||
INCONSISTENT,
|
||||
|
||||
/**
|
||||
* The local group was successfully updated to be consistent with the message revision
|
||||
*/
|
||||
GROUP_UPDATED,
|
||||
|
||||
/**
|
||||
* The local group is already consistent with the message revision or is ahead of the message revision
|
||||
*/
|
||||
GROUP_CONSISTENT_OR_AHEAD
|
||||
}
|
||||
|
||||
public static class GroupUpdateResult {
|
||||
private final GroupState groupState;
|
||||
@Nullable private DecryptedGroup latestServer;
|
||||
|
||||
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
|
||||
this.groupState = groupState;
|
||||
this.latestServer = latestServer;
|
||||
}
|
||||
|
||||
public GroupState getGroupState() {
|
||||
return groupState;
|
||||
}
|
||||
|
||||
public @Nullable DecryptedGroup getLatestServer() {
|
||||
return latestServer;
|
||||
}
|
||||
}
|
||||
|
||||
public final class StateProcessorForGroup {
|
||||
private final GroupMasterKey masterKey;
|
||||
private final GroupId.V2 groupId;
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
|
||||
private StateProcessorForGroup(@NonNull GroupMasterKey groupMasterKey) {
|
||||
this.masterKey = groupMasterKey;
|
||||
this.groupId = GroupId.v2(masterKey);
|
||||
this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Using network where required, will attempt to bring the local copy of the group up to the revision specified.
|
||||
*
|
||||
* @param revision use {@link #LATEST} to get latest.
|
||||
*/
|
||||
@WorkerThread
|
||||
public GroupUpdateResult updateLocalGroupToRevision(final int revision,
|
||||
final long timestamp)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
if (localIsAtLeast(revision)) {
|
||||
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||
}
|
||||
|
||||
GlobalGroupState inputGroupState = queryServer();
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
|
||||
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
|
||||
|
||||
if (newLocalState == null || newLocalState == inputGroupState.getLocalState()) {
|
||||
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||
}
|
||||
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries());
|
||||
persistLearnedProfileKeys(inputGroupState);
|
||||
|
||||
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
||||
if (remainingWork.getHistory().size() > 0) {
|
||||
Log.i(TAG, String.format(Locale.US, "There are more versions on the server for this group, not applying at this time, V[%d..%d]", newLocalState.getVersion() + 1, remainingWork.getLatestVersionNumber()));
|
||||
}
|
||||
|
||||
return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true iff group exists locally and is at least the specified revision.
|
||||
*/
|
||||
private boolean localIsAtLeast(int revision) {
|
||||
if (groupDatabase.isUnknownGroup(groupId) || revision == LATEST) {
|
||||
return false;
|
||||
}
|
||||
int dbRevision = groupDatabase.getGroup(groupId).get().requireV2GroupProperties().getGroupRevision();
|
||||
return revision <= dbRevision;
|
||||
}
|
||||
|
||||
private void updateLocalDatabaseGroupState(@NonNull GlobalGroupState inputGroupState,
|
||||
@NonNull DecryptedGroup newLocalState)
|
||||
{
|
||||
if (inputGroupState.getLocalState() == null) {
|
||||
groupDatabase.create(masterKey, newLocalState);
|
||||
} else {
|
||||
groupDatabase.update(masterKey, newLocalState);
|
||||
}
|
||||
|
||||
String avatar = newLocalState.getAvatar();
|
||||
if (!avatar.isEmpty()) {
|
||||
jobManager.add(new AvatarGroupsV2DownloadJob(groupId, avatar));
|
||||
}
|
||||
|
||||
final boolean fullMemberPostUpdate = GroupProtoUtil.isMember(Recipient.self().getUuid().get(), newLocalState.getMembersList());
|
||||
if (fullMemberPostUpdate) {
|
||||
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) {
|
||||
for (GroupLogEntry entry : processedLogEntries) {
|
||||
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange()), timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
|
||||
final ProfileKeySet profileKeys = new ProfileKeySet();
|
||||
|
||||
for (GroupLogEntry entry : globalGroupState.getHistory()) {
|
||||
profileKeys.addKeysFromGroupState(entry.getGroup(), DecryptedGroupUtil.editorUuid(entry.getChange()));
|
||||
}
|
||||
|
||||
Collection<RecipientId> updated = recipientDatabase.persistProfileKeySet(profileKeys);
|
||||
|
||||
if (!updated.isEmpty()) {
|
||||
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, scheduling profile retrievals", updated.size()));
|
||||
for (RecipientId recipient : updated) {
|
||||
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GlobalGroupState queryServer()
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
DecryptedGroup latestServerGroup;
|
||||
List<GroupLogEntry> history;
|
||||
UUID selfUuid = Recipient.self().getUuid().get();
|
||||
DecryptedGroup localState = groupDatabase.getGroup(groupId)
|
||||
.transform(g -> g.requireV2GroupProperties().getDecryptedGroup())
|
||||
.orNull();
|
||||
|
||||
try {
|
||||
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization);
|
||||
} catch (NotInGroupException e) {
|
||||
throw new GroupNotAMemberException(e);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
int versionWeWereAdded = GroupProtoUtil.findVersionWeWereAdded(latestServerGroup, selfUuid);
|
||||
int logsNeededFrom = localState != null ? Math.max(localState.getVersion(), versionWeWereAdded) : versionWeWereAdded;
|
||||
|
||||
if (GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) {
|
||||
history = getFullMemberHistory(selfUuid, logsNeededFrom);
|
||||
} else {
|
||||
history = Collections.singletonList(new GroupLogEntry(latestServerGroup, null));
|
||||
}
|
||||
|
||||
return new GlobalGroupState(localState, history);
|
||||
}
|
||||
|
||||
private List<GroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFrom) throws IOException {
|
||||
try {
|
||||
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFrom, groupsV2Authorization);
|
||||
ArrayList<GroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
|
||||
|
||||
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
|
||||
history.add(new GroupLogEntry(entry.getGroup(), entry.getChange()));
|
||||
}
|
||||
|
||||
return history;
|
||||
} catch (InvalidGroupStateException | VerificationFailedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
||||
try {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
mmsDatabase.markAsSent(messageId, true);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.jobs;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
|
@ -47,7 +46,14 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
|||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
groupId,
|
||||
cdnKey);
|
||||
requireNonEmpty(cdnKey));
|
||||
}
|
||||
|
||||
private static String requireNonEmpty(@NonNull String string) {
|
||||
if (string.isEmpty()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
private AvatarGroupsV2DownloadJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId, @NonNull String cdnKey) {
|
||||
|
@ -102,7 +108,7 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
|||
database.onAvatarUpdated(groupId, true);
|
||||
}
|
||||
|
||||
} catch (NonSuccessfulResponseCodeException | VerificationFailedException e) {
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
if (attachment != null && attachment.exists())
|
||||
|
|
|
@ -90,6 +90,7 @@ public final class JobManagerFactories {
|
|||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory());
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
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.logging.Log;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RequestGroupV2InfoJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "RequestGroupV2InfoJob";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(RequestGroupV2InfoJob.class);
|
||||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
private static final String KEY_TO_REVISION = "to_revision";
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final int toRevision;
|
||||
|
||||
public RequestGroupV2InfoJob(@NonNull GroupId.V2 groupId, int toRevision) {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue("RequestGroupV2InfoJob::" + groupId)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
groupId,
|
||||
toRevision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest group state for group.
|
||||
*/
|
||||
public RequestGroupV2InfoJob(@NonNull GroupId.V2 groupId) {
|
||||
this(groupId, GroupsV2StateProcessor.LATEST);
|
||||
}
|
||||
|
||||
private RequestGroupV2InfoJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId, int toRevision) {
|
||||
super(parameters);
|
||||
|
||||
this.groupId = groupId;
|
||||
this.toRevision = toRevision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString())
|
||||
.putInt(KEY_TO_REVISION, toRevision)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException {
|
||||
Log.i(TAG, "Updating group to revision " + toRevision);
|
||||
|
||||
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
|
||||
|
||||
if (!group.isPresent()) {
|
||||
Log.w(TAG, "Group not found");
|
||||
return;
|
||||
}
|
||||
|
||||
GroupManager.updateGroupFromServer(context, groupId, toRevision);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException ||
|
||||
e instanceof NoCredentialForRedemptionTimeException ||
|
||||
e instanceof GroupChangeBusyException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RequestGroupV2InfoJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull RequestGroupV2InfoJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RequestGroupV2InfoJob(parameters,
|
||||
GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2(),
|
||||
data.getInt(KEY_TO_REVISION));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -457,7 +457,13 @@ public class Util {
|
|||
|
||||
public static void assertMainThread() {
|
||||
if (!isMainThread()) {
|
||||
throw new AssertionError("Main-thread assertion failed.");
|
||||
throw new AssertionError("Must run on main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNotMainThread() {
|
||||
if (isMainThread()) {
|
||||
throw new AssertionError("Cannot run on main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -502,6 +502,7 @@
|
|||
|
||||
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
||||
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
|
||||
<string name="ManageGroupActivity_youre_not_a_member_of_the_group">You\'re not a member of the group</string>
|
||||
|
||||
<string name="ManageGroupActivity_edit_name_and_picture">Edit name and picture</string>
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.junit.runner.RunWith;
|
|||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
|
@ -21,6 +20,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -538,7 +538,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
}
|
||||
|
||||
ChangeBuilder promote(@NonNull UUID pendingMember) {
|
||||
builder.addPromotePendingMembers(UuidUtil.toByteString(pendingMember));
|
||||
builder.addPromotePendingMembers(DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(pendingMember)));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -555,8 +555,8 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
}
|
||||
|
||||
ChangeBuilder timer(int duration) {
|
||||
builder.setNewTimer(DisappearingMessagesTimer.newBuilder()
|
||||
.setDuration(duration));
|
||||
builder.setNewTimer(DecryptedTimer.newBuilder()
|
||||
.setDuration(duration));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -575,11 +575,11 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
}
|
||||
}
|
||||
|
||||
private ChangeBuilder changeBy(@NonNull UUID groupEditor) {
|
||||
private static ChangeBuilder changeBy(@NonNull UUID groupEditor) {
|
||||
return new ChangeBuilder(groupEditor);
|
||||
}
|
||||
|
||||
private @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map<UUID, String> map) {
|
||||
private static @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map<UUID, String> map) {
|
||||
return uuid -> {
|
||||
String name = map.get(uuid);
|
||||
assertNotNull(name);
|
||||
|
|
|
@ -58,7 +58,7 @@ public final class GroupIdTest {
|
|||
|
||||
GroupId.V2 groupId = GroupId.v2(new GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")));
|
||||
|
||||
assertEquals("__signal_group__v2__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e", groupId.toString());
|
||||
assertEquals("__signal_group__v2__!8c4a5ec277691282f64b965b1b9affc0285380c993c413f7560967d502dcf2e6", groupId.toString());
|
||||
assertFalse(groupId.isMms());
|
||||
assertFalse(groupId.isV1());
|
||||
assertTrue(groupId.isV2());
|
||||
|
|
|
@ -372,11 +372,11 @@ dependencyVerification {
|
|||
['org.signal:signal-metadata-java:0.1.2',
|
||||
'6aaeb6a33bf3161a3e6ac9db7678277f7a4cf5a2c96b84342e4007ee49bab1bd'],
|
||||
|
||||
['org.signal:zkgroup-android:0.4.1',
|
||||
'52049e207531ef50160873ad4a44682ce2bfb7706e5e4179035d9632eb9d5eac'],
|
||||
['org.signal:zkgroup-android:0.7.0',
|
||||
'52b172565bd01526e93ebf1796b834bdc449d4fe3422c1b827e49cb8d4f13fbd'],
|
||||
|
||||
['org.signal:zkgroup-java:0.4.1',
|
||||
'12ea7e18c58aaefdbb8eccb748deff4f7c8fbd950eeb9c426dc894de50a83b77'],
|
||||
['org.signal:zkgroup-java:0.7.0',
|
||||
'd0099eedd60d6f7d4df5b288175e5d585228ed8897789926bdab69bf8c05659f'],
|
||||
|
||||
['org.threeten:threetenbp:1.3.6',
|
||||
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
|
||||
|
|
|
@ -36,7 +36,7 @@ dependencies {
|
|||
api 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||
implementation 'org.threeten:threetenbp:1.3.6'
|
||||
|
||||
api 'org.signal:zkgroup-java:0.4.1'
|
||||
api 'org.signal:zkgroup-java:0.7.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||
|
|
|
@ -2,10 +2,13 @@ package org.whispersystems.signalservice.api.groupsv2;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
@ -14,6 +17,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -71,11 +75,11 @@ public final class DecryptedGroupUtil {
|
|||
* The UUID of the member that made the change.
|
||||
*/
|
||||
public static UUID editorUuid(DecryptedGroupChange change) {
|
||||
return UuidUtil.fromByteString(change.getEditor());
|
||||
return change != null ? UuidUtil.fromByteStringOrUnknown(change.getEditor()) : UuidUtil.UNKNOWN_UUID;
|
||||
}
|
||||
|
||||
public static Optional<DecryptedMember> findMemberByUuid(Collection<DecryptedMember> members, UUID uuid) {
|
||||
ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
ByteString uuidBytes = UuidUtil.toByteString(uuid);
|
||||
|
||||
for (DecryptedMember member : members) {
|
||||
if (uuidBytes.equals(member.getUuid())) {
|
||||
|
@ -87,7 +91,7 @@ public final class DecryptedGroupUtil {
|
|||
}
|
||||
|
||||
public static Optional<DecryptedPendingMember> findPendingByUuid(Collection<DecryptedPendingMember> members, UUID uuid) {
|
||||
ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
ByteString uuidBytes = UuidUtil.toByteString(uuid);
|
||||
|
||||
for (DecryptedPendingMember member : members) {
|
||||
if (uuidBytes.equals(member.getUuid())) {
|
||||
|
@ -98,6 +102,28 @@ public final class DecryptedGroupUtil {
|
|||
return Optional.absent();
|
||||
}
|
||||
|
||||
private static int findPendingIndexByUuidCipherText(List<DecryptedPendingMember> members, ByteString cipherText) {
|
||||
for (int i = 0; i < members.size(); i++) {
|
||||
DecryptedPendingMember member = members.get(i);
|
||||
if (cipherText.equals(member.getUuidCipherText())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findPendingIndexByUuid(List<DecryptedPendingMember> members, ByteString uuid) {
|
||||
for (int i = 0; i < members.size(); i++) {
|
||||
DecryptedPendingMember member = members.get(i);
|
||||
if (uuid.equals(member.getUuid())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the uuid from the full members of a group.
|
||||
* <p>
|
||||
|
@ -127,4 +153,105 @@ public final class DecryptedGroupUtil {
|
|||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
public static DecryptedGroup apply(DecryptedGroup group, DecryptedGroupChange change)
|
||||
throws NotAbleToApplyChangeException
|
||||
{
|
||||
if (change.getVersion() != group.getVersion() + 1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
|
||||
|
||||
builder.addAllMembers(change.getNewMembersList());
|
||||
|
||||
for (ByteString removedMember : change.getDeleteMembersList()) {
|
||||
int index = indexOfUuid(builder.getMembersList(), removedMember);
|
||||
|
||||
if (index == -1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
builder.removeMembers(index);
|
||||
}
|
||||
|
||||
for (DecryptedModifyMemberRole modifyMemberRole : change.getModifyMemberRolesList()) {
|
||||
int index = indexOfUuid(builder.getMembersList(), modifyMemberRole.getUuid());
|
||||
|
||||
if (index == -1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index)).setRole(modifyMemberRole.getRole()).build());
|
||||
}
|
||||
|
||||
for (DecryptedMember modifyProfileKey : change.getModifiedProfileKeysList()) {
|
||||
int index = indexOfUuid(builder.getMembersList(), modifyProfileKey.getUuid());
|
||||
|
||||
if (index == -1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
builder.setMembers(index, modifyProfileKey);
|
||||
}
|
||||
|
||||
for (DecryptedPendingMemberRemoval removedMember : change.getDeletePendingMembersList()) {
|
||||
int index = findPendingIndexByUuidCipherText(builder.getPendingMembersList(), removedMember.getUuidCipherText());
|
||||
|
||||
if (index == -1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
builder.removePendingMembers(index);
|
||||
}
|
||||
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
int index = findPendingIndexByUuid(builder.getPendingMembersList(), newMember.getUuid());
|
||||
|
||||
if (index == -1) {
|
||||
throw new NotAbleToApplyChangeException();
|
||||
}
|
||||
|
||||
builder.removePendingMembers(index);
|
||||
builder.addMembers(newMember);
|
||||
}
|
||||
|
||||
builder.addAllPendingMembers(change.getNewPendingMembersList());
|
||||
|
||||
if (change.hasNewTitle()) {
|
||||
builder.setTitle(change.getNewTitle().getValue());
|
||||
}
|
||||
|
||||
if (change.hasNewAvatar()) {
|
||||
builder.setAvatar(change.getNewAvatar().getValue());
|
||||
}
|
||||
|
||||
if (change.hasNewTimer()) {
|
||||
builder.setDisappearingMessagesTimer(change.getNewTimer());
|
||||
}
|
||||
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
builder.setAccessControl(AccessControl.newBuilder(builder.getAccessControl())
|
||||
.setAttributesValue(change.getNewAttributeAccessValue())
|
||||
.build());
|
||||
}
|
||||
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
builder.setAccessControl(AccessControl.newBuilder(builder.getAccessControl())
|
||||
.setMembersValue(change.getNewMemberAccessValue())
|
||||
.build());
|
||||
}
|
||||
|
||||
return builder.setVersion(change.getVersion()).build();
|
||||
}
|
||||
|
||||
private static int indexOfUuid(List<DecryptedMember> memberList, ByteString uuid) {
|
||||
for (int i = 0; i < memberList.size(); i++) {
|
||||
if(uuid.equals(memberList.get(i).getUuid())) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static class NotAbleToApplyChangeException extends Throwable {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,12 +169,12 @@ public final class GroupChangeUtil {
|
|||
}
|
||||
|
||||
private static void resolveField9PromotePendingMembers(DecryptedGroupChange conflictingChange, GroupChange.Actions.Builder result, HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid) {
|
||||
List<ByteString> promotePendingMembersList = conflictingChange.getPromotePendingMembersList();
|
||||
List<DecryptedMember> promotePendingMembersList = conflictingChange.getPromotePendingMembersList();
|
||||
|
||||
for (int i = promotePendingMembersList.size() - 1; i >= 0; i--) {
|
||||
ByteString member = promotePendingMembersList.get(i);
|
||||
DecryptedMember member = promotePendingMembersList.get(i);
|
||||
|
||||
if (!pendingMembersByUuid.containsKey(member)) {
|
||||
if (!pendingMembersByUuid.containsKey(member.getUuid())) {
|
||||
result.removePromotePendingMembers(i);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
|
@ -86,7 +89,7 @@ public final class GroupsV2Api {
|
|||
|
||||
byte[] cipherText;
|
||||
try {
|
||||
cipherText = new ClientZkGroupCipher(groupSecretParams).encryptBlob(avatar);
|
||||
cipherText = new ClientZkGroupCipher(groupSecretParams).encryptBlob(GroupAttributeBlob.newBuilder().setAvatar(ByteString.copyFrom(avatar)).build().toByteArray());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import com.google.protobuf.ByteString;
|
|||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.PendingMember;
|
||||
|
@ -16,6 +16,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.NotarySignature;
|
||||
import org.signal.zkgroup.ServerPublicParams;
|
||||
|
@ -30,10 +31,10 @@ import org.signal.zkgroup.profiles.ProfileKey;
|
|||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -46,8 +47,10 @@ import java.util.UUID;
|
|||
*/
|
||||
public final class GroupsV2Operations {
|
||||
|
||||
private static final String TAG = GroupsV2Operations.class.getSimpleName();
|
||||
|
||||
/** Used for undecryptable pending invites */
|
||||
public static final UUID UNKNOWN_UUID = new UUID(0, 0);
|
||||
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
|
||||
|
||||
private final ServerPublicParams serverPublicParams;
|
||||
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||
|
@ -222,7 +225,7 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
|
||||
public DecryptedGroup decryptGroup(Group group)
|
||||
throws VerificationFailedException, InvalidGroupStateException, InvalidProtocolBufferException
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
List<Member> membersList = group.getMembersList();
|
||||
List<PendingMember> pendingMembersList = group.getPendingMembersList();
|
||||
|
@ -237,20 +240,15 @@ public final class GroupsV2Operations {
|
|||
decryptedPendingMembers.add(decryptMember(member));
|
||||
}
|
||||
|
||||
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder()
|
||||
.setTitle(decryptTitle(group.getTitle()))
|
||||
.setAvatar(group.getAvatar())
|
||||
.setAccessControl(group.getAccessControl())
|
||||
.setVersion(group.getVersion())
|
||||
.addAllMembers(decryptedMembers)
|
||||
.addAllPendingMembers(decryptedPendingMembers);
|
||||
|
||||
DisappearingMessagesTimer messagesTimer = decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer());
|
||||
if (messagesTimer != null) {
|
||||
builder.setDisappearingMessagesTimer(messagesTimer);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
return DecryptedGroup.newBuilder()
|
||||
.setTitle(decryptTitle(group.getTitle()))
|
||||
.setAvatar(group.getAvatar())
|
||||
.setAccessControl(group.getAccessControl())
|
||||
.setVersion(group.getVersion())
|
||||
.addAllMembers(decryptedMembers)
|
||||
.addAllPendingMembers(decryptedPendingMembers)
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer())))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -261,7 +259,7 @@ public final class GroupsV2Operations {
|
|||
* are not signed, but should be empty.
|
||||
*/
|
||||
public DecryptedGroupChange decryptChange(GroupChange groupChange, boolean verify)
|
||||
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
|
||||
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
GroupChange.Actions actions = verify ? getVerifiedActions(groupChange) : getActions(groupChange);
|
||||
|
||||
|
@ -269,12 +267,22 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
|
||||
public DecryptedGroupChange decryptChange(GroupChange.Actions actions)
|
||||
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
return decryptChange(actions, null);
|
||||
}
|
||||
|
||||
public DecryptedGroupChange decryptChange(GroupChange.Actions actions, UUID source)
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
DecryptedGroupChange.Builder builder = DecryptedGroupChange.newBuilder();
|
||||
|
||||
// Field 1
|
||||
builder.setEditor(decryptUuidToByteString(actions.getSourceUuid()));
|
||||
if (source != null) {
|
||||
builder.setEditor(UuidUtil.toByteString(source));
|
||||
} else {
|
||||
builder.setEditor(decryptUuidToByteString(actions.getSourceUuid()));
|
||||
}
|
||||
|
||||
// Field 2
|
||||
builder.setVersion(actions.getVersion());
|
||||
|
@ -283,7 +291,9 @@ public final class GroupsV2Operations {
|
|||
for (GroupChange.Actions.AddMemberAction addMemberAction : actions.getAddMembersList()) {
|
||||
UUID uuid = decryptUuid(addMemberAction.getAdded().getUserId());
|
||||
builder.addNewMembers(DecryptedMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setJoinedAtVersion(actions.getVersion())
|
||||
.setRole(addMemberAction.getAdded().getRole())
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(decryptProfileKeyToByteString(addMemberAction.getAdded().getProfileKey(), uuid)));
|
||||
}
|
||||
|
||||
|
@ -309,7 +319,7 @@ public final class GroupsV2Operations {
|
|||
builder.addModifiedProfileKeys(DecryptedMember.newBuilder()
|
||||
.setRole(Member.Role.UNKNOWN)
|
||||
.setJoinedAtVersion(-1)
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(ByteString.copyFrom(decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid).serialize())));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new InvalidGroupStateException(e);
|
||||
|
@ -324,7 +334,7 @@ public final class GroupsV2Operations {
|
|||
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
|
||||
|
||||
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(uuidCipherText)
|
||||
.setRole(member.getRole())
|
||||
.setAddedByUuid(decryptUuidToByteString(added.getAddedByUserId()))
|
||||
|
@ -337,7 +347,7 @@ public final class GroupsV2Operations {
|
|||
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
|
||||
|
||||
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(uuidCipherText));
|
||||
}
|
||||
|
||||
|
@ -349,8 +359,13 @@ public final class GroupsV2Operations {
|
|||
} catch (InvalidInputException e) {
|
||||
throw new InvalidGroupStateException(e);
|
||||
}
|
||||
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
|
||||
builder.addPromotePendingMembers(UuidUtil.toByteString(uuid));
|
||||
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
|
||||
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
|
||||
builder.addPromotePendingMembers(DecryptedMember.newBuilder()
|
||||
.setJoinedAtVersion(-1)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
|
||||
}
|
||||
|
||||
// Field 10
|
||||
|
@ -365,8 +380,8 @@ public final class GroupsV2Operations {
|
|||
|
||||
// Field 12
|
||||
if (actions.hasModifyDisappearingMessagesTimer()) {
|
||||
int duration = decryptDisappearingMessagesTimer(actions.getModifyDisappearingMessagesTimer().getTimer()).getDuration();
|
||||
builder.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(duration));
|
||||
int duration = decryptDisappearingMessagesTimer(actions.getModifyDisappearingMessagesTimer().getTimer());
|
||||
builder.setNewTimer(DecryptedTimer.newBuilder().setDuration(duration));
|
||||
}
|
||||
|
||||
// Field 13
|
||||
|
@ -383,27 +398,28 @@ public final class GroupsV2Operations {
|
|||
}
|
||||
|
||||
private DecryptedMember decryptMember(Member member)
|
||||
throws InvalidGroupStateException, VerificationFailedException
|
||||
throws InvalidGroupStateException, VerificationFailedException
|
||||
{
|
||||
ByteString userId = member.getUserId();
|
||||
UUID uuid = decryptUuid(userId);
|
||||
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setJoinedAtVersion(member.getJoinedAtVersion())
|
||||
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
|
||||
.setRole(member.getRole())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DecryptedPendingMember decryptMember(PendingMember member)
|
||||
throws InvalidGroupStateException, VerificationFailedException
|
||||
throws InvalidGroupStateException, VerificationFailedException
|
||||
{
|
||||
ByteString userIdCipherText = member.getMember().getUserId();
|
||||
UUID uuid = decryptUuidOrUnknown(userIdCipherText);
|
||||
UUID addedBy = decryptUuid(member.getAddedByUserId());
|
||||
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(userIdCipherText)
|
||||
.setAddedByUuid(ByteString.copyFrom(UUIDUtil.serialize(addedBy)))
|
||||
.setRole(member.getMember().getRole())
|
||||
|
@ -452,43 +468,52 @@ public final class GroupsV2Operations {
|
|||
|
||||
private ByteString encryptTitle(String title) {
|
||||
try {
|
||||
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob((title == null ? "" : title).getBytes(StandardCharsets.UTF_8)));
|
||||
GroupAttributeBlob blob = GroupAttributeBlob.newBuilder().setTitle(title).build();
|
||||
|
||||
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(blob.toByteArray()));
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptTitle(ByteString cipherText) throws VerificationFailedException {
|
||||
return new String(decryptBlob(cipherText), StandardCharsets.UTF_8);
|
||||
private String decryptTitle(ByteString cipherText) {
|
||||
return decryptBlob(cipherText).getTitle();
|
||||
}
|
||||
|
||||
private DisappearingMessagesTimer decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage)
|
||||
throws VerificationFailedException, InvalidProtocolBufferException
|
||||
{
|
||||
return DisappearingMessagesTimer.parseFrom(decryptBlob(encryptedTimerMessage));
|
||||
private int decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage) {
|
||||
return decryptBlob(encryptedTimerMessage).getDisappearingMessagesDuration();
|
||||
}
|
||||
|
||||
private byte[] decryptBlob(ByteString blob) throws VerificationFailedException {
|
||||
public byte[] decryptAvatar(byte[] bytes) {
|
||||
return decryptBlob(bytes).getAvatar().toByteArray();
|
||||
}
|
||||
|
||||
private GroupAttributeBlob decryptBlob(ByteString blob) {
|
||||
return decryptBlob(blob.toByteArray());
|
||||
}
|
||||
|
||||
public byte[] decryptAvatar(byte[] bytes) throws VerificationFailedException {
|
||||
return decryptBlob(bytes);
|
||||
}
|
||||
|
||||
private byte[] decryptBlob(byte[] bytes) throws VerificationFailedException {
|
||||
private GroupAttributeBlob decryptBlob(byte[] bytes) {
|
||||
// TODO GV2: Minimum field length checking should be responsibility of clientZkGroupCipher#decryptBlob
|
||||
if (bytes == null) return null;
|
||||
if (bytes.length == 0) return bytes;
|
||||
if (bytes.length < 28) throw new VerificationFailedException();
|
||||
return clientZkGroupCipher.decryptBlob(bytes);
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return GroupAttributeBlob.getDefaultInstance();
|
||||
}
|
||||
if (bytes.length < 29) {
|
||||
Log.w(TAG, "Bad encrypted blob length");
|
||||
return GroupAttributeBlob.getDefaultInstance();
|
||||
}
|
||||
try {
|
||||
return GroupAttributeBlob.parseFrom(clientZkGroupCipher.decryptBlob(bytes));
|
||||
} catch (InvalidProtocolBufferException | VerificationFailedException e) {
|
||||
Log.w(TAG, "Bad encrypted blob");
|
||||
return GroupAttributeBlob.getDefaultInstance();
|
||||
}
|
||||
}
|
||||
|
||||
private ByteString encryptTimer(int timerDurationSeconds) {
|
||||
try {
|
||||
DisappearingMessagesTimer timer = DisappearingMessagesTimer.newBuilder()
|
||||
.setDuration(timerDurationSeconds)
|
||||
.build();
|
||||
GroupAttributeBlob timer = GroupAttributeBlob.newBuilder()
|
||||
.setDisappearingMessagesDuration(timerDurationSeconds)
|
||||
.build();
|
||||
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(timer.toByteArray()));
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new AssertionError(e);
|
||||
|
@ -499,7 +524,7 @@ public final class GroupsV2Operations {
|
|||
* Verifies signature and parses actions on a group change.
|
||||
*/
|
||||
private GroupChange.Actions getVerifiedActions(GroupChange groupChange)
|
||||
throws VerificationFailedException, InvalidProtocolBufferException
|
||||
throws VerificationFailedException, InvalidProtocolBufferException
|
||||
{
|
||||
byte[] actionsByteArray = groupChange.getActions().toByteArray();
|
||||
|
||||
|
@ -519,7 +544,7 @@ public final class GroupsV2Operations {
|
|||
* Parses actions on a group change without verification.
|
||||
*/
|
||||
private GroupChange.Actions getActions(GroupChange groupChange)
|
||||
throws InvalidProtocolBufferException
|
||||
throws InvalidProtocolBufferException
|
||||
{
|
||||
return GroupChange.Actions.parseFrom(groupChange.getActions());
|
||||
}
|
||||
|
@ -535,6 +560,13 @@ public final class GroupsV2Operations {
|
|||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder()
|
||||
.setAttributesAccess(newRights));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createChangeMemberRole(UUID uuid, Member.Role role) {
|
||||
return GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder()
|
||||
.setUserId(encryptUuid(uuid))
|
||||
.setRole(role));
|
||||
}
|
||||
}
|
||||
|
||||
public static class NewGroup {
|
||||
|
|
|
@ -13,6 +13,8 @@ import java.util.regex.Pattern;
|
|||
|
||||
public final class UuidUtil {
|
||||
|
||||
public static final UUID UNKNOWN_UUID = new UUID(0, 0);
|
||||
|
||||
private static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private UuidUtil() { }
|
||||
|
@ -57,6 +59,19 @@ public final class UuidUtil {
|
|||
return parseOrThrow(bytes.toByteArray());
|
||||
}
|
||||
|
||||
public static UUID fromByteStringOrNull(ByteString bytes) {
|
||||
return parseOrNull(bytes.toByteArray());
|
||||
}
|
||||
|
||||
public static UUID fromByteStringOrUnknown(ByteString bytes) {
|
||||
UUID uuid = parseOrNull(bytes.toByteArray());
|
||||
return uuid != null ? uuid : UNKNOWN_UUID;
|
||||
}
|
||||
|
||||
private static UUID parseOrNull(byte[] byteArray) {
|
||||
return byteArray != null && byteArray.length == 16 ? parseOrThrow(byteArray) : null;
|
||||
}
|
||||
|
||||
public static List<UUID> fromByteStrings(Collection<ByteString> byteStringCollection) {
|
||||
ArrayList<UUID> result = new ArrayList<>(byteStringCollection.size());
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequ
|
|||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
|
@ -1551,6 +1552,12 @@ public class PushServiceSocket {
|
|||
|
||||
private ResponseBody makeStorageRequest(String authorization, String path, String method, RequestBody body)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
return makeStorageRequest(authorization, path, method, body, NO_HANDLER);
|
||||
}
|
||||
|
||||
private ResponseBody makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(storageClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
|
@ -1594,6 +1601,8 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
responseCodeHandler.handle(response.code());
|
||||
|
||||
switch (response.code()) {
|
||||
case 204:
|
||||
throw new NoContentException("No content!");
|
||||
|
@ -1826,73 +1835,69 @@ public class PushServiceSocket {
|
|||
return JsonUtil.fromJson(response, CredentialResponse.class);
|
||||
}
|
||||
|
||||
private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = NO_HANDLER;
|
||||
private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = NO_HANDLER;
|
||||
private static final ResponseCodeHandler GROUPS_V2_GET_LOGS_HANDLER = NO_HANDLER;
|
||||
private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = responseCode -> {
|
||||
if (responseCode == 403) throw new NotInGroupException();
|
||||
};
|
||||
|
||||
public void putNewGroupsV2Group(Group group, String authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
makeStorageRequest(authorization,
|
||||
GROUPSV2_GROUP,
|
||||
"PUT",
|
||||
protobufRequestBody(group));
|
||||
protobufRequestBody(group),
|
||||
GROUPS_V2_PUT_RESPONSE_HANDLER);
|
||||
}
|
||||
|
||||
public Group getGroupsV2Group(String authorization)
|
||||
throws IOException
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
ResponseBody response = makeStorageRequest(authorization,
|
||||
GROUPSV2_GROUP,
|
||||
"GET",
|
||||
null);
|
||||
null,
|
||||
GROUPS_V2_GET_CURRENT_HANDLER);
|
||||
|
||||
try {
|
||||
return Group.parseFrom(readBodyBytes(response));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new IOException("Cannot read protobuf", e);
|
||||
}
|
||||
return Group.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
public AvatarUploadAttributes getGroupsV2AvatarUploadForm(String authorization)
|
||||
throws IOException
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
ResponseBody response = makeStorageRequest(authorization,
|
||||
GROUPSV2_AVATAR_REQUEST,
|
||||
"GET",
|
||||
null);
|
||||
null,
|
||||
NO_HANDLER);
|
||||
|
||||
try {
|
||||
return AvatarUploadAttributes.parseFrom(readBodyBytes(response));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new IOException("Cannot read protobuf", e);
|
||||
}
|
||||
return AvatarUploadAttributes.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization)
|
||||
throws IOException
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
ResponseBody response = makeStorageRequest(authorization,
|
||||
GROUPSV2_GROUP,
|
||||
"PATCH",
|
||||
protobufRequestBody(groupChange));
|
||||
protobufRequestBody(groupChange),
|
||||
GROUPS_V2_PATCH_RESPONSE_HANDLER);
|
||||
|
||||
try {
|
||||
return GroupChange.parseFrom(readBodyBytes(response));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new IOException("Cannot read protobuf", e);
|
||||
}
|
||||
return GroupChange.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
public GroupChanges getGroupsV2GroupHistory(int fromVersion, String authorization)
|
||||
throws IOException
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
ResponseBody response = makeStorageRequest(authorization,
|
||||
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion),
|
||||
"GET",
|
||||
null);
|
||||
null,
|
||||
GROUPS_V2_GET_LOGS_HANDLER);
|
||||
|
||||
try {
|
||||
return GroupChanges.parseFrom(readBodyBytes(response));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new IOException("Cannot read protobuf", e);
|
||||
}
|
||||
return GroupChanges.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
private final class ResumeInfo {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
public final class NotInGroupException extends NonSuccessfulResponseCodeException {
|
||||
}
|
|
@ -40,13 +40,13 @@ message DecryptedModifyMemberRole {
|
|||
// Decrypted version of message Group
|
||||
// Keep field numbers in step
|
||||
message DecryptedGroup {
|
||||
string title = 2;
|
||||
string avatar = 3;
|
||||
DisappearingMessagesTimer disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 version = 6;
|
||||
repeated DecryptedMember members = 7;
|
||||
repeated DecryptedPendingMember pendingMembers = 8;
|
||||
string title = 2;
|
||||
string avatar = 3;
|
||||
DecryptedTimer disappearingMessagesTimer = 4;
|
||||
AccessControl accessControl = 5;
|
||||
uint32 version = 6;
|
||||
repeated DecryptedMember members = 7;
|
||||
repeated DecryptedPendingMember pendingMembers = 8;
|
||||
}
|
||||
|
||||
// Decrypted version of message GroupChange.Actions
|
||||
|
@ -60,10 +60,10 @@ message DecryptedGroupChange {
|
|||
repeated DecryptedMember modifiedProfileKeys = 6;
|
||||
repeated DecryptedPendingMember newPendingMembers = 7;
|
||||
repeated DecryptedPendingMemberRemoval deletePendingMembers = 8;
|
||||
repeated bytes promotePendingMembers = 9;
|
||||
repeated DecryptedMember promotePendingMembers = 9;
|
||||
DecryptedString newTitle = 10;
|
||||
DecryptedString newAvatar = 11;
|
||||
DisappearingMessagesTimer newTimer = 12;
|
||||
DecryptedTimer newTimer = 12;
|
||||
AccessControl.AccessRequired newAttributeAccess = 13;
|
||||
AccessControl.AccessRequired newMemberAccess = 14;
|
||||
}
|
||||
|
@ -71,3 +71,7 @@ message DecryptedGroupChange {
|
|||
message DecryptedString {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message DecryptedTimer {
|
||||
uint32 duration = 1;
|
||||
}
|
||||
|
|
|
@ -143,6 +143,10 @@ message GroupChanges {
|
|||
repeated GroupChangeState groupChanges = 1;
|
||||
}
|
||||
|
||||
message DisappearingMessagesTimer {
|
||||
uint32 duration = 1;
|
||||
message GroupAttributeBlob {
|
||||
oneof content {
|
||||
string title = 1;
|
||||
bytes avatar = 2;
|
||||
uint32 disappearingMessagesDuration = 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.junit.Test;
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -17,7 +18,7 @@ public final class DecryptedGroupUtilTest {
|
|||
public void can_extract_uuid_from_decrypted_member() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
DecryptedMember decryptedMember = DecryptedMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.build();
|
||||
|
||||
UUID parsed = DecryptedGroupUtil.toUuid(decryptedMember);
|
||||
|
@ -28,7 +29,7 @@ public final class DecryptedGroupUtilTest {
|
|||
@Test
|
||||
public void can_extract_editor_uuid_from_decrypted_group_change() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
ByteString editor = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
ByteString editor = UuidUtil.toByteString(uuid);
|
||||
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(editor)
|
||||
.build();
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
|
||||
|
||||
public final class DecryptedGroupUtil_apply_Test {
|
||||
|
||||
@Test
|
||||
public void apply_version() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(9)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(10)
|
||||
.build());
|
||||
|
||||
assertEquals(10, newGroup.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_new_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedMember member2 = member(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.addMembers(member1)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.addNewMembers(member2)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_remove_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedMember member2 = member(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(13)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(14)
|
||||
.addDeleteMembers(member1.getUuid())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(14)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_remove_members() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedMember member2 = member(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(13)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(14)
|
||||
.addDeleteMembers(member1.getUuid())
|
||||
.addDeleteMembers(member2.getUuid())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(14)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test(expected = DecryptedGroupUtil.NotAbleToApplyChangeException.class)
|
||||
public void apply_remove_members_not_found() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedMember member2 = member(UUID.randomUUID());
|
||||
|
||||
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(13)
|
||||
.addMembers(member1)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(14)
|
||||
.addDeleteMembers(member2.getUuid())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_modify_member_role() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedMember member2 = admin(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(13)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(14)
|
||||
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder().setUuid(member1.getUuid()).setRole(Member.Role.ADMINISTRATOR))
|
||||
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder().setUuid(member2.getUuid()).setRole(Member.Role.DEFAULT))
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(14)
|
||||
.addMembers(asAdmin(member1))
|
||||
.addMembers(asMember(member2))
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_modify_member_profile_keys() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
ProfileKey profileKey1 = randomProfileKey();
|
||||
ProfileKey profileKey2a = randomProfileKey();
|
||||
ProfileKey profileKey2b = randomProfileKey();
|
||||
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
|
||||
DecryptedMember member2a = member(UUID.randomUUID(), profileKey2a);
|
||||
DecryptedMember member2b = withProfileKey(member2a, profileKey2b);
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(13)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2a)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(14)
|
||||
.addModifiedProfileKeys(member2b)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(14)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2b)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_new_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
DecryptedPendingMember pending = pendingMember(UUID.randomUUID());
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.addMembers(member1)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.addNewPendingMembers(pending)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.addMembers(member1)
|
||||
.addPendingMembers(pending)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
UUID pendingUuid = UUID.randomUUID();
|
||||
DecryptedPendingMember pending = pendingMember(pendingUuid);
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.addMembers(member1)
|
||||
.addPendingMembers(pending)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
|
||||
.setUuidCipherText(ProtoTestUtils.encrypt(pendingUuid))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.addMembers(member1)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promote_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
UUID pending2Uuid = UUID.randomUUID();
|
||||
DecryptedPendingMember pending2 = pendingMember(pending2Uuid);
|
||||
DecryptedMember member2 = member(pending2Uuid, profileKey2);
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.addMembers(member1)
|
||||
.addPendingMembers(pending2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.addPromotePendingMembers(member2)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promote_direct_to_admin() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedMember member1 = member(UUID.randomUUID());
|
||||
UUID pending2Uuid = UUID.randomUUID();
|
||||
DecryptedPendingMember pending2 = pendingMember(pending2Uuid);
|
||||
DecryptedMember member2 = withProfileKey(admin(pending2Uuid), profileKey2);
|
||||
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.addMembers(member1)
|
||||
.addPendingMembers(pending2)
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.addPromotePendingMembers(member2)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.addMembers(member1)
|
||||
.addMembers(member2)
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void title() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setTitle("Old title")
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewTitle(DecryptedString.newBuilder().setValue("New title").build())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setTitle("New title")
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void avatar() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setAvatar("https://cnd/oldavatar")
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewAvatar(DecryptedString.newBuilder().setValue("https://cnd/newavatar").build())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setAvatar("https://cnd/newavatar")
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timer() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(100))
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewTimer(DecryptedTimer.newBuilder().setDuration(2000))
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(2000))
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attribute_access() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.build())
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.build())
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void membership_access() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.build())
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.setMembers(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build())
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void change_both_access_levels() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setVersion(10)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER)
|
||||
.build())
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setVersion(11)
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setVersion(11)
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setMembers(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build())
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
}
|
|
@ -4,27 +4,30 @@ import com.google.protobuf.ByteString;
|
|||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.PendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encrypt;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||
|
||||
public final class GroupChangeUtil_resolveConflict_Test {
|
||||
|
||||
|
@ -257,9 +260,9 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
.addPendingMembers(pendingMember(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member1))
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member2))
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member3))
|
||||
.addPromotePendingMembers(member(member1))
|
||||
.addPromotePendingMembers(member(member2))
|
||||
.addPromotePendingMembers(member(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member1, randomProfileKey())))
|
||||
|
@ -370,10 +373,10 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
@Test
|
||||
public void field_12__timer_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDisappearingMessagesTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(456))
|
||||
.setNewTimer(DecryptedTimer.newBuilder().setDuration(456))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().setTimer(ByteString.EMPTY))
|
||||
|
@ -387,10 +390,10 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
@Test
|
||||
public void field_12__no_timer_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDisappearingMessagesTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.setNewTimer(DecryptedTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().setTimer(ByteString.EMPTY))
|
||||
|
@ -468,92 +471,4 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
private static ProfileKey randomProfileKey() {
|
||||
byte[] contents = new byte[32];
|
||||
new SecureRandom().nextBytes(contents);
|
||||
try {
|
||||
return new ProfileKey(contents);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates encryption by creating a unique {@link ByteString} that won't equal a byte string created from the {@link UUID}.
|
||||
*/
|
||||
private static ByteString encrypt(UUID uuid) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
return ByteString.copyFrom(Arrays.copyOf(uuidBytes, uuidBytes.length + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a presentation by concatenating the uuid and profile key which makes it suitable for
|
||||
* equality assertions in these tests.
|
||||
*/
|
||||
private static ByteString presentation(UUID uuid, ProfileKey profileKey) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
byte[] profileKeyBytes = profileKey.serialize();
|
||||
byte[] concat = new byte[uuidBytes.length + profileKeyBytes.length];
|
||||
|
||||
System.arraycopy(uuidBytes, 0, concat, 0, uuidBytes.length);
|
||||
System.arraycopy(profileKeyBytes, 0, concat, uuidBytes.length, profileKeyBytes.length);
|
||||
|
||||
return ByteString.copyFrom(concat);
|
||||
}
|
||||
|
||||
private static DecryptedModifyMemberRole promoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedModifyMemberRole demoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Member encryptedMember(UUID uuid, ProfileKey profileKey) {
|
||||
return Member.newBuilder()
|
||||
.setPresentation(presentation(uuid, profileKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember member(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedPendingMemberRemoval pendingMemberRemoval(UUID uuid) {
|
||||
return DecryptedPendingMemberRemoval.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedPendingMember pendingMember(UUID uuid) {
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember admin(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
final class ProtoTestUtils {
|
||||
|
||||
static ProfileKey randomProfileKey() {
|
||||
byte[] contents = new byte[32];
|
||||
new SecureRandom().nextBytes(contents);
|
||||
try {
|
||||
return new ProfileKey(contents);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates encryption by creating a unique {@link ByteString} that won't equal a byte string created from the {@link UUID}.
|
||||
*/
|
||||
static ByteString encrypt(UUID uuid) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
return ByteString.copyFrom(Arrays.copyOf(uuidBytes, uuidBytes.length + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a presentation by concatenating the uuid and profile key which makes it suitable for
|
||||
* equality assertions in these tests.
|
||||
*/
|
||||
static ByteString presentation(UUID uuid, ProfileKey profileKey) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
byte[] profileKeyBytes = profileKey.serialize();
|
||||
byte[] concat = new byte[uuidBytes.length + profileKeyBytes.length];
|
||||
|
||||
System.arraycopy(uuidBytes, 0, concat, 0, uuidBytes.length);
|
||||
System.arraycopy(profileKeyBytes, 0, concat, uuidBytes.length, profileKeyBytes.length);
|
||||
|
||||
return ByteString.copyFrom(concat);
|
||||
}
|
||||
|
||||
static DecryptedModifyMemberRole promoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedModifyMemberRole demoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
static Member encryptedMember(UUID uuid, ProfileKey profileKey) {
|
||||
return Member.newBuilder()
|
||||
.setPresentation(presentation(uuid, profileKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember member(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedPendingMemberRemoval pendingMemberRemoval(UUID uuid) {
|
||||
return DecryptedPendingMemberRemoval.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(encrypt(uuid))
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedPendingMember pendingMember(UUID uuid) {
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setUuidCipherText(encrypt(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
|
||||
return withProfileKey(member(uuid), profileKey);
|
||||
}
|
||||
|
||||
static DecryptedMember admin(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember withProfileKey(DecryptedMember member, ProfileKey profileKey) {
|
||||
return DecryptedMember.newBuilder(member)
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember asAdmin(DecryptedMember member) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(member.getUuid())
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
static DecryptedMember asMember(DecryptedMember member) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(member.getUuid())
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -27,8 +27,8 @@ dependencyVerification {
|
|||
['org.signal:signal-metadata-java:0.1.2',
|
||||
'6aaeb6a33bf3161a3e6ac9db7678277f7a4cf5a2c96b84342e4007ee49bab1bd'],
|
||||
|
||||
['org.signal:zkgroup-java:0.4.1',
|
||||
'12ea7e18c58aaefdbb8eccb748deff4f7c8fbd950eeb9c426dc894de50a83b77'],
|
||||
['org.signal:zkgroup-java:0.7.0',
|
||||
'd0099eedd60d6f7d4df5b288175e5d585228ed8897789926bdab69bf8c05659f'],
|
||||
|
||||
['org.threeten:threetenbp:1.3.6',
|
||||
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
|
||||
|
|
Ładowanie…
Reference in New Issue