GV2 database.

master
Alan Evans 2020-03-27 15:55:44 -03:00 committed by Greyson Parrelli
parent 640c82d517
commit 9e6cca1cd0
23 changed files with 513 additions and 78 deletions

View File

@ -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<GroupData> doInBackground(GroupId... groupIds) {
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], false);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
existingContacts.addAll(recipients);

View File

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

View File

@ -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<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false);
List<Recipient> 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));

View File

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

View File

@ -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<RecipientId> members, boolean mms) {
public GroupId.Mms getOrCreateMmsGroupForMembers(List<RecipientId> 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<Recipient> getGroupMembers(@NonNull GroupId groupId, boolean includeSelf) {
List<RecipientId> members = getCurrentMembers(groupId);
List<Recipient> recipients = new LinkedList<>();
@WorkerThread
public @NonNull List<Recipient> 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<RecipientId> currentMembers = getCurrentMembers(groupId);
List<Recipient> 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<RecipientId> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
public void create(@NonNull GroupId.V1 groupId,
@Nullable String title,
@NonNull Collection<RecipientId> members,
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay)
{
create(groupId, title, members, avatar, relay, null, null);
}
public void create(@NonNull GroupId.Mms groupId,
@NonNull Collection<RecipientId> 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<RecipientId> memberCollection,
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay,
@Nullable GroupMasterKey groupMasterKey,
@Nullable DecryptedGroup groupState)
{
List<RecipientId> 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<RecipientId> 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<RecipientId> 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<RecipientId> 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<Recipient> getMembers(@NonNull Context context, @NonNull MemberSet memberSet) {
boolean includeSelf = memberSet.includeSelf;
DecryptedGroup groupV2 = getDecryptedGroup();
UUID selfUuid = Recipient.self().getUuid().get();
List<Recipient> 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;
}
}
}

View File

@ -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<RecipientId> recipientIds, long mmsId, int status, long timestamp) {
public void insert(Collection<RecipientId> recipientIds, long mmsId, int status, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
for (RecipientId recipientId : recipientIds) {

View File

@ -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<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false);
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
List<Recipient> 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());

View File

@ -233,7 +233,7 @@ public class SmsMigrator {
List<RecipientId> 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);

View File

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

View File

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

View File

@ -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<RecipientId> recordMembers = new HashSet<>(groupRecord.getMembers());
Set<RecipientId> messageMembers = new HashSet<>();

View File

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

View File

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

View File

@ -232,7 +232,7 @@ public class MmsDownloadJob extends BaseJob {
if (members.size() > 2) {
List<RecipientId> 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);

View File

@ -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<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false);
List<Recipient> 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;

View File

@ -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<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return Stream.of(members).map(Recipient::getId).toList();
}

View File

@ -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<Recipient> messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList();
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, false);
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
Map<RecipientId, Integer> localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId))
.collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus));

View File

@ -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<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), false);
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : recipients) {
handleIndividualRecipient(recipient);

View File

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

View File

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

View File

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

View File

@ -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<UUID> toUuidSet(Collection<DecryptedMember> membersList) {
HashSet<UUID> uuids = new HashSet<>(membersList.size());
for (DecryptedMember member : membersList) {
uuids.add(toUuid(member));
}
return uuids;
}
public static ArrayList<UUID> toUuidList(Collection<DecryptedMember> membersList) {
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
for (DecryptedMember member : membersList) {
uuidList.add(toUuid(member));
}
return uuidList;
}
public static ArrayList<UUID> pendingToUuidList(Collection<DecryptedPendingMember> membersList) {
ArrayList<UUID> 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<DecryptedMember> findMemberByUuid(Collection<DecryptedMember> 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<DecryptedPendingMember> findPendingByUuid(Collection<DecryptedPendingMember> 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.
* <p>
* 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<DecryptedMember> decryptedMembers = new ArrayList<>(builder.getMembersList());
Iterator<DecryptedMember> 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;
}
}
}

View File

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