From 9e6cca1cd0a9ba55f0bbf132b3433f7e11e82cfa Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Fri, 27 Mar 2020 15:55:44 -0300 Subject: [PATCH] GV2 database. --- .../securesms/GroupCreateActivity.java | 4 +- .../securesms/GroupMembersDialog.java | 3 +- .../securesms/MessageDetailsActivity.java | 3 +- .../conversation/ConversationActivity.java | 3 +- .../securesms/database/GroupDatabase.java | 305 +++++++++++++++--- .../database/GroupReceiptDatabase.java | 4 +- .../securesms/database/MmsDatabase.java | 2 +- .../securesms/database/SmsMigrator.java | 2 +- .../database/helpers/SQLCipherOpenHelper.java | 9 +- .../securesms/groups/GroupId.java | 12 + .../groups/GroupV1MessageProcessor.java | 4 +- .../securesms/groups/V1GroupManager.java | 25 +- .../securesms/jobs/AvatarDownloadJob.java | 8 +- .../securesms/jobs/MmsDownloadJob.java | 2 +- .../securesms/jobs/MmsSendJob.java | 6 +- .../securesms/jobs/PushGroupSendJob.java | 3 +- .../securesms/jobs/PushProcessMessageJob.java | 2 +- .../securesms/jobs/RetrieveProfileJob.java | 3 +- .../securesms/jobs/TypingSendJob.java | 3 +- .../org/thoughtcrime/securesms/util/Util.java | 10 +- .../securesms/groups/GroupIdTest.java | 17 + .../internal/groupsv2/DecryptedGroupUtil.java | 120 +++++++ .../groupsv2/DecryptedGroupUtilTest.java | 41 +++ 23 files changed, 513 insertions(+), 78 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java index fec674656..796f4ab33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -364,7 +364,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity } memberAddresses.add(Recipient.self().getId()); - GroupId groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true); + GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses); RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); @@ -550,7 +550,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity @Override protected Optional doInBackground(GroupId... groupIds) { final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity); - final List recipients = db.getGroupMembers(groupIds[0], false); + final List recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); final Optional group = db.getGroup(groupIds[0]); final Set existingContacts = new HashSet<>(recipients.size()); existingContacts.addAll(recipients); diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java index 6112a71da..a25129edc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java @@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.Lifecycle; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.recipients.Recipient; @@ -34,7 +35,7 @@ public final class GroupMembersDialog { public void display() { SimpleTask.run( lifecycle, - () -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), true), + () -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF), members -> { AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.ConversationActivity_group_members) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java index 6cf6deab5..b82463605 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -31,6 +31,7 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.Loader; import org.thoughtcrime.securesms.conversation.ConversationItem; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.logging.Log; import android.view.LayoutInflater; import android.view.MenuItem; @@ -368,7 +369,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity List receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId()); if (receiptInfoList.isEmpty()) { - List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false); + List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient recipient : group) { recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 8c2e864dd..58bfa8d3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -132,6 +132,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; @@ -1574,7 +1575,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (params[0].isGroup()) { recipients.addAll(DatabaseFactory.getGroupDatabase(ConversationActivity.this) - .getGroupMembers(params[0].requireGroupId(), false)); + .getGroupMembers(params[0].requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)); } else { recipients.add(params[0]); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index b997ccfbd..a0ada225e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -1,39 +1,50 @@ package org.thoughtcrime.securesms.database; - import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; +import com.google.protobuf.InvalidProtocolBufferException; import net.sqlcipher.database.SQLiteDatabase; +import org.signal.storageservice.protos.groups.Member; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil; import java.io.Closeable; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.UUID; -public class GroupDatabase extends Database { +public final class GroupDatabase extends Database { - @SuppressWarnings("unused") - private static final String TAG = GroupDatabase.class.getSimpleName(); + private static final String TAG = Log.tag(GroupDatabase.class); static final String TABLE_NAME = "groups"; private static final String ID = "_id"; @@ -50,6 +61,14 @@ public class GroupDatabase extends Database { static final String ACTIVE = "active"; static final String MMS = "mms"; + /* V2 Group columns */ + /** 32 bytes serialized {@link GroupMasterKey} */ + private static final String V2_MASTER_KEY = "master_key"; + /** Increments with every change to the group */ + private static final String V2_REVISION = "revision"; + /** Serialized {@link DecryptedGroup} protobuf */ + private static final String V2_DECRYPTED_GROUP = "decrypted_group"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + @@ -64,7 +83,10 @@ public class GroupDatabase extends Database { TIMESTAMP + " INTEGER, " + ACTIVE + " INTEGER DEFAULT 1, " + AVATAR_DIGEST + " BLOB, " + - MMS + " INTEGER DEFAULT 0);"; + MMS + " INTEGER DEFAULT 0, " + + V2_MASTER_KEY + " BLOB, " + + V2_REVISION + " BLOB, " + + V2_DECRYPTED_GROUP + " BLOB);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", @@ -140,19 +162,20 @@ public class GroupDatabase extends Database { return new Reader(cursor); } - public GroupId getOrCreateGroupForMembers(List members, boolean mms) { + public GroupId.Mms getOrCreateMmsGroupForMembers(List members) { Collections.sort(members); Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID}, MEMBERS + " = ? AND " + MMS + " = ?", - new String[] {RecipientId.toSerializedList(members), mms ? "1" : "0"}, + new String[] {RecipientId.toSerializedList(members), "1"}, null, null, null); try { if (cursor != null && cursor.moveToNext()) { - return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))); + return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))) + .requireMms(); } else { - GroupId groupId = allocateGroupId(mms); - create(groupId, null, members, null, null); + GroupId.Mms groupId = GroupId.createMms(new SecureRandom()); + create(groupId, members); return groupId; } } finally { @@ -194,24 +217,62 @@ public class GroupDatabase extends Database { return new Reader(cursor); } - public @NonNull List getGroupMembers(@NonNull GroupId groupId, boolean includeSelf) { - List members = getCurrentMembers(groupId); - List recipients = new LinkedList<>(); + @WorkerThread + public @NonNull List getGroupMembers(@NonNull GroupId groupId, @NonNull MemberSet memberSet) { + if (groupId.isV2()) { + return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMembers(context, memberSet)) + .or(Collections.emptyList()); + } else { + List currentMembers = getCurrentMembers(groupId); + List recipients = new ArrayList<>(currentMembers.size()); - for (RecipientId member : members) { - if (!includeSelf && Recipient.resolved(member).isLocalNumber()) { - continue; + for (RecipientId member : currentMembers) { + if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) { + recipients.add(Recipient.resolved(member)); + } } - recipients.add(Recipient.resolved(member)); + return recipients; } - - return recipients; } - public void create(@NonNull GroupId groupId, @Nullable String title, @NonNull List members, - @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay) + public void create(@NonNull GroupId.V1 groupId, + @Nullable String title, + @NonNull Collection members, + @Nullable SignalServiceAttachmentPointer avatar, + @Nullable String relay) { + create(groupId, title, members, avatar, relay, null, null); + } + + public void create(@NonNull GroupId.Mms groupId, + @NonNull Collection members) + { + 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) + { + create(groupId, groupState.getTitle(), Collections.emptyList(), avatar, relay, groupMasterKey, groupState); + } + + /** + * @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version). + */ + private void create(@NonNull GroupId groupId, + @Nullable String title, + @NonNull Collection memberCollection, + @Nullable SignalServiceAttachmentPointer avatar, + @Nullable String relay, + @Nullable GroupMasterKey groupMasterKey, + @Nullable DecryptedGroup groupState) + { + List members = new ArrayList<>(new HashSet<>(memberCollection)); + Collections.sort(members); ContentValues contentValues = new ContentValues(); @@ -234,6 +295,21 @@ public class GroupDatabase extends Database { contentValues.put(ACTIVE, 1); contentValues.put(MMS, groupId.isMms()); + if (groupMasterKey != null) { + if (groupState == null) { + throw new AssertionError("V2 master key but no group state"); + } + groupId.requireV2(); + contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize()); + contentValues.put(V2_REVISION, groupState.getVersion()); + contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray()); + contentValues.put(MEMBERS, serializeV2GroupMembers(context, groupState)); + } else { + if (groupId.isV2()) { + throw new AssertionError("V2 group id but no master key"); + } + } + databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); @@ -242,7 +318,10 @@ public class GroupDatabase extends Database { notifyConversationListListeners(); } - public void update(@NonNull GroupId groupId, String title, SignalServiceAttachmentPointer avatar) { + public void update(@NonNull GroupId.V1 groupId, + @Nullable String title, + @Nullable SignalServiceAttachmentPointer avatar) + { ContentValues contentValues = new ContentValues(); if (title != null) contentValues.put(TITLE, title); @@ -265,7 +344,30 @@ public class GroupDatabase extends Database { notifyConversationListListeners(); } - public void updateTitle(@NonNull GroupId groupId, String title) { + public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) { + update(GroupId.v2(groupMasterKey), decryptedGroup); + } + + public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) { + String title = decryptedGroup.getTitle(); + ContentValues contentValues = new ContentValues(); + + contentValues.put(TITLE, title); + contentValues.put(V2_REVISION, decryptedGroup.getVersion()); + contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray()); + contentValues.put(MEMBERS, serializeV2GroupMembers(context, decryptedGroup)); + + databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, + GROUP_ID + " = ?", + new String[]{ groupId.toString() }); + + RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); + Recipient.live(groupRecipient).refresh(); + + notifyConversationListListeners(); + } + + public void updateTitle(@NonNull GroupId.V1 groupId, String title) { ContentValues contentValues = new ContentValues(); contentValues.put(TITLE, title); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", @@ -278,7 +380,7 @@ public class GroupDatabase extends Database { /** * Used to bust the Glide cache when an avatar changes. */ - public void onAvatarUpdated(@NonNull GroupId groupId, boolean hasAvatar) { + public void onAvatarUpdated(@NonNull GroupId.V1 groupId, boolean hasAvatar) { ContentValues contentValues = new ContentValues(1); contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0); @@ -350,10 +452,18 @@ public class GroupDatabase extends Database { database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()}); } - public static GroupId allocateGroupId(boolean mms) { - byte[] groupId = new byte[16]; - new SecureRandom().nextBytes(groupId); - return mms ? GroupId.mms(groupId) : GroupId.v1(groupId); + private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) { + List groupMembers = new ArrayList<>(decryptedGroup.getMembersCount()); + + for (DecryptedMember member : decryptedGroup.getMembersList()) { + Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.fromByteString(member.getUuid()), null)); + + groupMembers.add(recipient.getId()); + } + + Collections.sort(groupMembers); + + return RecipientId.toSerializedList(groupMembers); } public static class Reader implements Closeable { @@ -387,7 +497,10 @@ public class GroupDatabase extends Database { cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)), cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1, cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)), - cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1); + cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1, + cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)), + cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)), + cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP))); } @Override @@ -399,21 +512,23 @@ public class GroupDatabase extends Database { public static class GroupRecord { - private final GroupId id; - private final RecipientId recipientId; - private final String title; - private final List members; - private final long avatarId; - private final byte[] avatarKey; - private final byte[] avatarDigest; - private final String avatarContentType; - private final String relay; - private final boolean active; - private final boolean mms; + private final GroupId id; + private final RecipientId recipientId; + private final String title; + private final List members; + private final long avatarId; + private final byte[] avatarKey; + private final byte[] avatarDigest; + private final String avatarContentType; + private final String relay; + private final boolean active; + private final boolean mms; + @Nullable private final V2GroupProperties v2GroupProperties; public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, long avatarId, byte[] avatarKey, String avatarContentType, - String relay, boolean active, byte[] avatarDigest, boolean mms) + String relay, boolean active, byte[] avatarDigest, boolean mms, + @Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes) { this.id = id; this.recipientId = recipientId; @@ -426,6 +541,18 @@ public class GroupDatabase extends Database { this.active = active; this.mms = mms; + V2GroupProperties v2GroupProperties = null; + if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { + GroupMasterKey groupMasterKey; + try { + groupMasterKey = new GroupMasterKey(groupMasterKeyBytes); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + v2GroupProperties = new V2GroupProperties(groupMasterKey, groupRevision, decryptedGroupBytes); + } + this.v2GroupProperties = v2GroupProperties; + if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members); else this.members = new LinkedList<>(); } @@ -477,5 +604,97 @@ public class GroupDatabase extends Database { public boolean isMms() { return mms; } + + public boolean isV2Group() { + return v2GroupProperties != null; + } + + public @NonNull V2GroupProperties requireV2GroupProperties() { + if (v2GroupProperties == null) { + throw new AssertionError(); + } + + return v2GroupProperties; + } + + public boolean isAdmin(@NonNull Recipient recipient) { + return isV2Group() && requireV2GroupProperties().isAdmin(recipient); + } + } + + public static class V2GroupProperties { + + @NonNull private final GroupMasterKey groupMasterKey; + private final int groupRevision; + @NonNull private final byte[] decryptedGroupBytes; + private DecryptedGroup decryptedGroup; + + private V2GroupProperties(@NonNull GroupMasterKey groupMasterKey, int groupRevision, @NonNull byte[] decryptedGroup) { + this.groupMasterKey = groupMasterKey; + this.groupRevision = groupRevision; + this.decryptedGroupBytes = decryptedGroup; + } + + public @NonNull GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + + public int getGroupRevision() { + return groupRevision; + } + + public @NonNull DecryptedGroup getDecryptedGroup() { + try { + if (decryptedGroup == null) { + decryptedGroup = DecryptedGroup.parseFrom(decryptedGroupBytes); + } + return decryptedGroup; + } catch (InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + } + + public boolean isAdmin(@NonNull Recipient recipient) { + return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), recipient.getUuid().get()) + .transform(t -> t.getRole() == Member.Role.ADMINISTRATOR) + .or(false); + } + + public List getMembers(@NonNull Context context, @NonNull MemberSet memberSet) { + boolean includeSelf = memberSet.includeSelf; + DecryptedGroup groupV2 = getDecryptedGroup(); + UUID selfUuid = Recipient.self().getUuid().get(); + List recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount()); + + for (UUID uuid : DecryptedGroupUtil.toUuidList(groupV2.getMembersList())) { + if (includeSelf || !selfUuid.equals(uuid)) { + recipients.add(Recipient.externalPush(context, uuid, null)); + } + } + if (memberSet.includePending) { + for (UUID uuid : DecryptedGroupUtil.pendingToUuidList(groupV2.getPendingMembersList())) { + if (includeSelf || !selfUuid.equals(uuid)) { + recipients.add(Recipient.externalPush(context, uuid, null)); + } + } + } + + return recipients; + } + } + + public enum MemberSet { + FULL_MEMBERS_INCLUDING_SELF(true, false), + FULL_MEMBERS_EXCLUDING_SELF(false, false), + FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), + FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true); + + private boolean includeSelf; + private boolean includePending; + + MemberSet(boolean includeSelf, boolean includePending) { + this.includeSelf = includeSelf; + this.includePending = includePending; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 440e4ee3c..fd038b98f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; + import androidx.annotation.NonNull; import net.sqlcipher.database.SQLiteDatabase; @@ -11,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.recipients.RecipientId; +import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -41,7 +43,7 @@ public class GroupReceiptDatabase extends Database { super(context, databaseHelper); } - public void insert(List recipientIds, long mmsId, int status, long timestamp) { + public void insert(Collection recipientIds, long mmsId, int status, long timestamp) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); for (RecipientId recipientId : recipientIds) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 020c1f3b4..a670a70ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1141,8 +1141,8 @@ public class MmsDatabase extends MessagingDatabase { long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener); if (message.getRecipient().isGroup()) { - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false); GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(), messageId, defaultReceiptStatus, message.getSentTimeMillis()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java index 01e47b2a0..57f935893 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java @@ -233,7 +233,7 @@ public class SmsMigrator { List recipientIds = Stream.of(ourRecipients).map(Recipient::getId).toList(); - GroupId ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipientIds, true); + GroupId.Mms ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipientIds); RecipientId ourGroupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(ourGroupId); Recipient ourGroupRecipient = Recipient.resolved(ourGroupRecipientId); long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index b1895111f..d59af0c7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -124,8 +124,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int TRANSFER_FILE_CLEANUP = 52; private static final int PROFILE_DATA_MIGRATION = 53; private static final int AVATAR_LOCATION_MIGRATION = 54; + private static final int GROUPS_V2 = 55; - private static final int DATABASE_VERSION = 54; + private static final int DATABASE_VERSION = 55; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -851,6 +852,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("UPDATE groups SET avatar = NULL"); } + if (oldVersion < GROUPS_V2) { + db.execSQL("ALTER TABLE groups ADD COLUMN master_key"); + db.execSQL("ALTER TABLE groups ADD COLUMN revision"); + db.execSQL("ALTER TABLE groups ADD COLUMN decrypted_group"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java index bba076950..9048e65cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java @@ -7,13 +7,17 @@ import org.signal.zkgroup.groups.GroupIdentifier; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; import org.thoughtcrime.securesms.util.Hex; +import org.thoughtcrime.securesms.util.Util; import java.io.IOException; +import java.security.SecureRandom; public abstract class GroupId { private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!"; private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!"; + private static final int MMS_BYTE_LENGTH = 16; + private static final int V1_MMS_BYTE_LENGTH = 16; private static final int V2_BYTE_LENGTH = GroupIdentifier.SIZE; private static final int V2_ENCODED_LENGTH = ENCODED_SIGNAL_GROUP_PREFIX.length() + V2_BYTE_LENGTH * 2; @@ -34,6 +38,14 @@ public abstract class GroupId { return new GroupId.V1(gv1GroupIdBytes); } + public static GroupId.V1 createV1(@NonNull SecureRandom secureRandom) { + return v1(Util.getSecretBytes(secureRandom, V1_MMS_BYTE_LENGTH)); + } + + public static GroupId.Mms createMms(@NonNull SecureRandom secureRandom) { + return mms(Util.getSecretBytes(secureRandom, MMS_BYTE_LENGTH)); + } + public static GroupId.V2 v2(@NonNull byte[] bytes) { if (bytes.length != V2_BYTE_LENGTH) { throw new AssertionError(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index 57e4e0b16..150e33f94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -93,7 +93,7 @@ public final class GroupV1MessageProcessor { boolean outgoing) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupId id = GroupId.v1(group.getGroupId()); + GroupId.V1 id = GroupId.v1(group.getGroupId()); GroupContext.Builder builder = createGroupContext(group); builder.setType(GroupContext.Type.UPDATE); @@ -127,7 +127,7 @@ public final class GroupV1MessageProcessor { { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupId id = GroupId.v1(group.getGroupId()); + GroupId.V1 id = GroupId.v1(group.getGroupId()); Set recordMembers = new HashSet<>(groupRecord.getMembers()); Set messageMembers = new HashSet<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index c04b19416..8480b3d8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -36,6 +36,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupC import java.io.ByteArrayInputStream; import java.io.IOException; +import java.security.SecureRandom; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -53,23 +54,28 @@ final class V1GroupManager { { final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - final GroupId groupId = GroupDatabase.allocateGroupId(mms); + final SecureRandom secureRandom = new SecureRandom(); + final GroupId groupId = mms ? GroupId.createMms(secureRandom) : GroupId.createV1(secureRandom); final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); final Recipient groupRecipient = Recipient.resolved(groupRecipientId); memberIds.add(Recipient.self().getId()); - groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null); - if (!mms) { + if (groupId.isV1()) { + GroupId.V1 groupIdV1 = groupId.requireV1(); + + groupDatabase.create(groupIdV1, name, memberIds, null, null); + try { AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); } catch (IOException e) { Log.w(TAG, "Failed to save avatar!", e); } - groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); + groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); - return sendGroupUpdate(context, groupId.requireV1(), memberIds, name, avatarBytes); + return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes); } else { + groupDatabase.create(groupId.requireMms(), memberIds); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); } @@ -88,16 +94,19 @@ final class V1GroupManager { memberAddresses.add(Recipient.self().getId()); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); - groupDatabase.updateTitle(groupId, name); - groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); if (groupId.isPush()) { + GroupId.V1 groupIdV1 = groupId.requireV1(); + + groupDatabase.updateTitle(groupIdV1, name); + groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null); + try { AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); } catch (IOException e) { Log.w(TAG, "Failed to save avatar!", e); } - return sendGroupUpdate(context, groupId.requireV1(), memberAddresses, name, avatarBytes); + return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes); } else { Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 7a5ace5d7..0dfbb72fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -38,9 +38,9 @@ public class AvatarDownloadJob extends BaseJob { private static final String KEY_GROUP_ID = "group_id"; - private @NonNull GroupId groupId; + private @NonNull GroupId.V1 groupId; - public AvatarDownloadJob(@NonNull GroupId groupId) { + public AvatarDownloadJob(@NonNull GroupId.V1 groupId) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(10) @@ -48,7 +48,7 @@ public class AvatarDownloadJob extends BaseJob { groupId); } - private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId groupId) { + private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId.V1 groupId) { super(parameters); this.groupId = groupId; } @@ -118,7 +118,7 @@ public class AvatarDownloadJob extends BaseJob { public static final class Factory implements Job.Factory { @Override public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID))); + return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)).requireV1()); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 5468db6da..65eceaaef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -232,7 +232,7 @@ public class MmsDownloadJob extends BaseJob { if (members.size() > 2) { List recipients = new ArrayList<>(members); - group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipients, true)); + group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients)); } IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java index 7371c1858..66979aed4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -25,6 +25,7 @@ import com.klinker.android.send_message.Utils; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.Util; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.security.SecureRandom; import java.util.Arrays; import java.util.List; @@ -231,7 +233,7 @@ public final class MmsSendJob extends SendJob { } if (message.getRecipient().isMmsGroup()) { - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient member : members) { if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) { @@ -271,7 +273,7 @@ public final class MmsSendJob extends SendJob { PduPart part = new PduPart(); if (fileName == null) { - fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong())); + fileName = String.valueOf(Math.abs(new SecureRandom().nextLong())); String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType()); if (fileExtension != null) fileName = fileName + "." + fileExtension; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 833659ac5..f95cdb331 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; @@ -299,7 +300,7 @@ public class PushGroupSendJob extends PushSendJob { List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList(); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); return Stream.of(members).map(Recipient::getId).toList(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index b767915c7..daf53f6c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -1030,7 +1030,7 @@ public final class PushProcessMessageJob extends BaseJob { private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) { GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); List messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList(); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); Map localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId)) .collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index f6528f6b7..a115ca367 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -10,6 +10,7 @@ import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -137,7 +138,7 @@ public class RetrieveProfileJob extends BaseJob { } private void handleGroupRecipient(Recipient group) throws IOException { - List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), false); + List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient recipient : recipients) { handleIndividualRecipient(recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java index fa3cd4b90..0ed2f8950 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -6,6 +6,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -85,7 +86,7 @@ public class TypingSendJob extends BaseJob { Optional groupId = Optional.absent(); if (recipient.isGroup()) { - recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), false); + recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); groupId = Optional.of(recipient.requireGroupId().getDecodedId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index c064198cd..a72c47940 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -428,13 +428,13 @@ public class Util { } public static byte[] getSecretBytes(int size) { - byte[] secret = new byte[size]; - getSecureRandom().nextBytes(secret); - return secret; + return getSecretBytes(new SecureRandom(), size); } - public static SecureRandom getSecureRandom() { - return new SecureRandom(); + public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size) { + byte[] secret = new byte[size]; + secureRandom.nextBytes(secret); + return secret; } public static int getDaysTillBuildExpiry() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java index 3d83c0abe..b835ddee0 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.thoughtcrime.securesms.groups.ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS; +import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom; public final class GroupIdTest { @@ -264,4 +265,20 @@ public final class GroupIdTest { public void cannot_create_v2_with_a_v1_length() throws IOException { GroupId.v2(Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f")); } + + @Test + public void create_mms() { + GroupId.Mms mms = GroupId.createMms(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 })); + + assertEquals("__signal_mms_group__!090a0b0c0d0e0f000102030405060708", mms.toString()); + assertTrue(mms.isMms()); + } + + @Test + public void create_v1() { + GroupId.V1 v1 = GroupId.createV1(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 })); + + assertEquals("__textsecure_group__!090a0b0c0d0e0f000102030405060708", v1.toString()); + assertTrue(v1.isV1()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java new file mode 100644 index 000000000..88cb3df05 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java @@ -0,0 +1,120 @@ +package org.whispersystems.signalservice.internal.groupsv2; + +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.util.UUIDUtil; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; + +public final class DecryptedGroupUtil { + + public static Set toUuidSet(Collection membersList) { + HashSet uuids = new HashSet<>(membersList.size()); + + for (DecryptedMember member : membersList) { + uuids.add(toUuid(member)); + } + + return uuids; + } + + public static ArrayList toUuidList(Collection membersList) { + ArrayList uuidList = new ArrayList<>(membersList.size()); + + for (DecryptedMember member : membersList) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + + public static ArrayList pendingToUuidList(Collection membersList) { + ArrayList uuidList = new ArrayList<>(membersList.size()); + + for (DecryptedPendingMember member : membersList) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + + public static UUID toUuid(DecryptedMember member) { + return UUIDUtil.deserialize(member.getUuid().toByteArray()); + } + + public static UUID toUuid(DecryptedPendingMember member) { + return UUIDUtil.deserialize(member.getUuid().toByteArray()); + } + + /** + * The UUID of the member that made the change. + */ + public static UUID editorUuid(DecryptedGroupChange change) { + return UuidUtil.fromByteString(change.getEditor()); + } + + public static Optional findMemberByUuid(Collection members, UUID uuid) { + ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + + for (DecryptedMember member : members) { + if (uuidBytes.equals(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.absent(); + } + + public static Optional findPendingByUuid(Collection members, UUID uuid) { + ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + + for (DecryptedPendingMember member : members) { + if (uuidBytes.equals(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.absent(); + } + + /** + * Removes the uuid from the full members of a group. + *

+ * Generally not expected to have to do this, just in the case of leaving a group where you cannot + * get the new group state as you are not in the group any longer. + */ + public static DecryptedGroup removeMember(DecryptedGroup group, UUID uuid, int revision) { + DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group); + ByteString uuidString = UuidUtil.toByteString(uuid); + boolean removed = false; + ArrayList decryptedMembers = new ArrayList<>(builder.getMembersList()); + Iterator membersList = decryptedMembers.iterator(); + + while (membersList.hasNext()) { + if (uuidString.equals(membersList.next().getUuid())) { + membersList.remove(); + removed = true; + } + } + + if (removed) { + return builder.clearMembers() + .addAllMembers(decryptedMembers) + .setVersion(revision) + .build(); + } else { + return group; + } + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java new file mode 100644 index 000000000..b17fc4ec6 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java @@ -0,0 +1,41 @@ +package org.whispersystems.signalservice.internal.groupsv2; + +import com.google.protobuf.ByteString; + +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 java.util.UUID; + +import static org.junit.Assert.assertEquals; + +public final class DecryptedGroupUtilTest { + + @Test + public void can_extract_uuid_from_decrypted_member() { + UUID uuid = UUID.randomUUID(); + DecryptedMember decryptedMember = DecryptedMember.newBuilder() + .setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid))) + .build(); + + UUID parsed = DecryptedGroupUtil.toUuid(decryptedMember); + + assertEquals(uuid, parsed); + } + + @Test + public void can_extract_editor_uuid_from_decrypted_group_change() { + UUID uuid = UUID.randomUUID(); + ByteString editor = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder() + .setEditor(editor) + .build(); + + UUID parsed = DecryptedGroupUtil.editorUuid(groupChange); + + assertEquals(uuid, parsed); + } + +}