Initial pre-alpha support for sender key.

This commit is contained in:
Greyson Parrelli
2021-05-14 14:03:35 -04:00
parent c54f016213
commit 57c0b8fd0f
124 changed files with 3668 additions and 444 deletions

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -41,31 +42,34 @@ public class DatabaseFactory {
private static volatile DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final OneTimePreKeyDatabase preKeyDatabase;
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final StickerDatabase stickerDatabase;
private final UnknownStorageIdDatabase storageIdDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
private final PaymentDatabase paymentDatabase;
private final ChatColorsDatabase chatColorsDatabase;
private final EmojiSearchDatabase emojiSearchDatabase;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final OneTimePreKeyDatabase preKeyDatabase;
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SenderKeyDatabase senderKeyDatabase;
private final SenderKeySharedDatabase senderKeySharedDatabase;
private final PendingRetryReceiptDatabase pendingRetryReceiptDatabase;
private final SearchDatabase searchDatabase;
private final StickerDatabase stickerDatabase;
private final UnknownStorageIdDatabase storageIdDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
private final PaymentDatabase paymentDatabase;
private final ChatColorsDatabase chatColorsDatabase;
private final EmojiSearchDatabase emojiSearchDatabase;
public static DatabaseFactory getInstance(Context context) {
if (instance == null) {
@@ -148,6 +152,18 @@ public class DatabaseFactory {
return getInstance(context).sessionDatabase;
}
public static SenderKeyDatabase getSenderKeyDatabase(Context context) {
return getInstance(context).senderKeyDatabase;
}
public static SenderKeySharedDatabase getSenderKeySharedDatabase(Context context) {
return getInstance(context).senderKeySharedDatabase;
}
public static PendingRetryReceiptDatabase getPendingRetryReceiptDatabase(Context context) {
return getInstance(context).pendingRetryReceiptDatabase;
}
public static SearchDatabase getSearchDatabase(Context context) {
return getInstance(context).searchDatabase;
}
@@ -210,31 +226,34 @@ public class DatabaseFactory {
DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context);
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.senderKeyDatabase = new SenderKeyDatabase(context, databaseHelper);
this.senderKeySharedDatabase = new SenderKeySharedDatabase(context, databaseHelper);
this.pendingRetryReceiptDatabase = new PendingRetryReceiptDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -20,7 +20,9 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
@@ -48,6 +50,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -71,6 +74,7 @@ public final class GroupDatabase extends Database {
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
private static final String DISTRIBUTION_ID = "distribution_id";
/* V2 Group columns */
@@ -98,15 +102,17 @@ public final class GroupDatabase extends Database {
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL);";
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
DISTRIBUTION_ID + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");"
};
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON " + TABLE_NAME + "(" + DISTRIBUTION_ID + ")"
};
private static final String[] GROUP_PROJECTION = {
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
};
@@ -256,6 +262,38 @@ public final class GroupDatabase extends Database {
return new Reader(cursor);
}
public @NonNull DistributionId getOrCreateDistributionId(@NonNull GroupId.V2 groupId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = GROUP_ID + " = ?";
String[] args = SqlUtil.buildArgs(groupId);
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { DISTRIBUTION_ID }, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
Optional<String> serialized = CursorUtil.getString(cursor, DISTRIBUTION_ID);
if (serialized.isPresent()) {
return DistributionId.from(serialized.get());
} else {
Log.w(TAG, "Missing distributionId! Creating one.");
DistributionId distributionId = DistributionId.create();
ContentValues values = new ContentValues(1);
values.put(DISTRIBUTION_ID, distributionId.toString());
int count = db.update(TABLE_NAME, values, query, args);
if (count < 1) {
throw new IllegalStateException("Tried to create a distributionId for " + groupId + ", but it doesn't exist!");
}
return distributionId;
}
} else {
throw new IllegalStateException("Group " + groupId + " doesn't exist!");
}
}
}
public GroupId.Mms getOrCreateMmsGroupForMembers(List<RecipientId> members) {
Collections.sort(members);
@@ -436,6 +474,7 @@ public final class GroupDatabase extends Database {
if (groupId.isV2()) {
contentValues.put(ACTIVE, groupState != null && gv2GroupActive(groupState) ? 1 : 0);
contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString());
} else if (groupId.isV1()) {
contentValues.put(ACTIVE, 1);
contentValues.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString());
@@ -524,6 +563,7 @@ public final class GroupDatabase extends Database {
ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, groupIdV2.toString());
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString());
contentValues.putNull(EXPECTED_V2_ID);
List<RecipientId> newMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()));
@@ -596,6 +636,18 @@ public final class GroupDatabase extends Database {
contentValues.put(MEMBERS, RecipientId.toSerializedList(groupMembers));
contentValues.put(ACTIVE, gv2GroupActive(decryptedGroup) ? 1 : 0);
DistributionId distributionId = Objects.requireNonNull(existingGroup.get().getDistributionId());
if (existingGroup.isPresent() && existingGroup.get().isV2Group()) {
DecryptedGroupChange change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().getDecryptedGroup(), decryptedGroup);
List<UUID> removed = DecryptedGroupUtil.removedMembersUuidList(change);
if (removed.size() > 0) {
Log.i(TAG, removed.size() + " members were removed from group " + groupId + ". Rotating the sender key.");
SenderKeyUtil.rotateOurKey(context, distributionId);
}
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
GROUP_ID + " = ?",
new String[]{ groupId.toString() });
@@ -604,7 +656,7 @@ public final class GroupDatabase extends Database {
recipientDatabase.setExpireMessages(groupRecipientId, decryptedGroup.getDisappearingMessagesTimer().getDuration());
}
if (groupMembers != null && (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing())) {
if (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing()) {
recipientDatabase.setHasGroupsInCommon(groupMembers);
}
@@ -735,7 +787,7 @@ public final class GroupDatabase extends Database {
}
private static List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
private static @NonNull List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
List<RecipientId> groupMembers = new ArrayList<>(uuids.size());
for (UUID uuid : uuids) {
@@ -751,11 +803,9 @@ public final class GroupDatabase extends Database {
return groupMembers;
}
private static List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
List<RecipientId> recipientIds = uuidsToRecipientIds(uuids);
return recipientIds;
private static @NonNull List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
return uuidsToRecipientIds(uuids);
}
public @NonNull List<GroupId.V2> getAllGroupV2Ids() {
@@ -830,7 +880,8 @@ public final class GroupDatabase extends Database {
CursorUtil.requireBoolean(cursor, MMS),
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
CursorUtil.requireInt(cursor, V2_REVISION),
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP));
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
CursorUtil.getString(cursor, DISTRIBUTION_ID).transform(DistributionId::from).orNull());
}
@Override
@@ -855,6 +906,7 @@ public final class GroupDatabase extends Database {
private final boolean active;
private final boolean mms;
@Nullable private final V2GroupProperties v2GroupProperties;
private final DistributionId distributionId;
public GroupRecord(@NonNull GroupId id,
@NonNull RecipientId recipientId,
@@ -870,7 +922,8 @@ public final class GroupDatabase extends Database {
boolean mms,
@Nullable byte[] groupMasterKeyBytes,
int groupRevision,
@Nullable byte[] decryptedGroupBytes)
@Nullable byte[] decryptedGroupBytes,
@Nullable DistributionId distributionId)
{
this.id = id;
this.recipientId = recipientId;
@@ -882,6 +935,7 @@ public final class GroupDatabase extends Database {
this.relay = relay;
this.active = active;
this.mms = mms;
this.distributionId = distributionId;
V2GroupProperties v2GroupProperties = null;
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
@@ -969,6 +1023,10 @@ public final class GroupDatabase extends Database {
return mms;
}
public @Nullable DistributionId getDistributionId() {
return distributionId;
}
public boolean isV1Group() {
return !mms && !isV2Group();
}

View File

@@ -9,12 +9,16 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.Nullable;
public class GroupReceiptDatabase extends Database {
public static final String TABLE_NAME = "group_receipts";
@@ -109,6 +113,23 @@ public class GroupReceiptDatabase extends Database {
return results;
}
public @Nullable GroupReceiptInfo getGroupReceiptInfo(long mmsId, @NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = MMS_ID + " = ? AND " + RECIPIENT_ID + " = ?";
String[] args = SqlUtil.buildArgs(mmsId, recipientId);
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, "1")) {
if (cursor.moveToFirst()) {
return new GroupReceiptInfo(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)),
cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1);
}
}
return null;
}
void deleteRowsForMessage(long mmsId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});

View File

@@ -158,7 +158,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException;
public abstract Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId);
public abstract Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException;
public abstract @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
public abstract @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
public abstract void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId);
public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener);
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;

View File

@@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
@@ -1443,7 +1442,12 @@ public class MmsDatabase extends MessageDatabase {
}
@Override
public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
throw new UnsupportedOperationException();
}
@Override
public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
throw new UnsupportedOperationException();
}

View File

@@ -74,6 +74,7 @@ public interface MmsSmsColumns {
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
protected static final long GROUP_CALL_TYPE = 12;
protected static final long BAD_DECRYPT_TYPE = 13;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
@@ -196,6 +197,10 @@ public interface MmsSmsColumns {
return (type & BASE_TYPE_MASK) == INVALID_MESSAGE_TYPE;
}
public static boolean isBadDecryptType(long type) {
return (type & BASE_TYPE_MASK) == BAD_DECRYPT_TYPE;
}
public static boolean isSecureType(long type) {
return (type & SECURE_MESSAGE_BIT) != 0;
}
@@ -298,7 +303,7 @@ public interface MmsSmsColumns {
return (type & GROUP_QUIT_BIT) != 0;
}
public static boolean isFailedDecryptType(long type) {
public static boolean isChatSessionRefresh(long type) {
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0;
}

View File

@@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
/**
* Holds information about messages we've sent out retry receipts for.
*/
public final class PendingRetryReceiptDatabase extends Database {
public static final String TABLE_NAME = "pending_retry_receipts";
private static final String ID = "_id";
private static final String AUTHOR = "author";
private static final String DEVICE = "device";
private static final String SENT_TIMESTAMP = "sent_timestamp";
private static final String RECEIVED_TIMESTAMP = "received_timestamp";
private static final String THREAD_ID = "thread_id";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
AUTHOR + " TEXT NOT NULL, " +
DEVICE + " INTEGER NOT NULL, " +
SENT_TIMESTAMP + " INTEGER NOT NULL, " +
RECEIVED_TIMESTAMP + " TEXT NOT NULL, " +
THREAD_ID + " INTEGER NOT NULL, " +
"UNIQUE(" + AUTHOR + "," + SENT_TIMESTAMP + ") ON CONFLICT REPLACE);";
PendingRetryReceiptDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void insert(@NonNull RecipientId author, int authorDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
ContentValues values = new ContentValues();
values.put(AUTHOR, author.serialize());
values.put(DEVICE, authorDevice);
values.put(SENT_TIMESTAMP, sentTimestamp);
values.put(RECEIVED_TIMESTAMP, receivedTimestamp);
values.put(THREAD_ID, threadId);
databaseHelper.getWritableDatabase().insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public @Nullable PendingRetryReceiptModel get(@NonNull RecipientId author, long sentTimestamp) {
String query = AUTHOR + " = ? AND " + SENT_TIMESTAMP + " = ?";
String[] args = SqlUtil.buildArgs(author, sentTimestamp);
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
return fromCursor(cursor);
}
}
return null;
}
public @Nullable PendingRetryReceiptModel getOldest() {
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, RECEIVED_TIMESTAMP + " ASC", "1")) {
if (cursor.moveToFirst()) {
return fromCursor(cursor);
}
}
return null;
}
public void delete(long id) {
databaseHelper.getWritableDatabase().delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(id));
}
private static @NonNull PendingRetryReceiptModel fromCursor(@NonNull Cursor cursor) {
return new PendingRetryReceiptModel(CursorUtil.requireLong(cursor, ID),
RecipientId.from(CursorUtil.requireString(cursor, AUTHOR)),
CursorUtil.requireInt(cursor, DEVICE),
CursorUtil.requireLong(cursor, SENT_TIMESTAMP),
CursorUtil.requireLong(cursor, RECEIVED_TIMESTAMP),
CursorUtil.requireLong(cursor, THREAD_ID));
}
}

View File

@@ -156,11 +156,16 @@ public class RecipientDatabase extends Database {
private static final String IDENTITY_STATUS = "identity_status";
private static final String IDENTITY_KEY = "identity_key";
/**
* Values that represent the index in the capabilities bitmask. Each index can store a 2-bit
* value, which in this case is the value of {@link Recipient.Capability}.
*/
private static final class Capabilities {
static final int BIT_LENGTH = 2;
static final int GROUPS_V2 = 0;
static final int GROUPS_V1_MIGRATION = 1;
static final int SENDER_KEY = 2;
}
private static final String[] RECIPIENT_PROJECTION = new String[] {
@@ -1632,6 +1637,7 @@ public class RecipientDatabase extends Database {
value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize());
value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize());
ContentValues values = new ContentValues(1);
values.put(CAPABILITIES, value);
@@ -3137,6 +3143,7 @@ public class RecipientDatabase extends Database {
private final long capabilities;
private final Recipient.Capability groupsV2Capability;
private final Recipient.Capability groupsV1MigrationCapability;
private final Recipient.Capability senderKeyCapability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
@@ -3145,9 +3152,9 @@ public class RecipientDatabase extends Database {
private final AvatarColor avatarColor;
private final String about;
private final String aboutEmoji;
private final SyncExtras syncExtras;
private final Recipient.Extras extras;
private final boolean hasGroupsInCommon;
private final SyncExtras syncExtras;
private final Recipient.Extras extras;
private final boolean hasGroupsInCommon;
RecipientSettings(@NonNull RecipientId id,
@Nullable UUID uuid,
@@ -3227,6 +3234,7 @@ public class RecipientDatabase extends Database {
this.capabilities = capabilities;
this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH));
this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH));
this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH));
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.mentionSetting = mentionSetting;
@@ -3376,6 +3384,10 @@ public class RecipientDatabase extends Database {
return groupsV1MigrationCapability;
}
public @NonNull Recipient.Capability getSenderKeyCapability() {
return senderKeyCapability;
}
public @Nullable byte[] getStorageId() {
return storageId;
}

View File

@@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
import java.io.IOException;
/**
* Stores all of the sender keys -- both the ones we create, and the ones we're told about.
*
* When working with SenderKeys, keep this in mind: they're not *really* keys. They're sessions.
* The name is largely historical, and there's too much momentum to change it.
*/
public class SenderKeyDatabase extends Database {
private static final String TAG = Log.tag(SenderKeyDatabase.class);
public static final String TABLE_NAME = "sender_keys";
private static final String ID = "_id";
public static final String RECIPIENT_ID = "recipient_id";
public static final String DEVICE = "device";
public static final String DISTRIBUTION_ID = "distribution_id";
public static final String RECORD = "record";
public static final String CREATED_AT = "created_at";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
RECIPIENT_ID + " INTEGER NOT NULL, " +
DEVICE + " INTEGER NOT NULL, " +
DISTRIBUTION_ID + " TEXT NOT NULL, " +
RECORD + " BLOB NOT NULL, " +
CREATED_AT + " INTEGER NOT NULL, " +
"UNIQUE(" + RECIPIENT_ID + "," + DEVICE + ", " + DISTRIBUTION_ID + ") ON CONFLICT REPLACE);";
SenderKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void store(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId, @NonNull SenderKeyRecord record) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(DEVICE, deviceId);
values.put(DISTRIBUTION_ID, distributionId.toString());
values.put(RECORD, record.serialize());
values.put(CREATED_AT, System.currentTimeMillis());
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public @Nullable SenderKeyRecord load(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?";
String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId);
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ RECORD }, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
try {
return new SenderKeyRecord(CursorUtil.requireBlob(cursor, RECORD));
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
return null;
}
/**
* Gets when the sender key session was created, or -1 if it doesn't exist.
*/
public long getCreatedTime(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?";
String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId);
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ CREATED_AT }, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
return CursorUtil.requireLong(cursor, CREATED_AT);
}
}
return -1;
}
/**
* Removes all sender key session state for all devices for the provided recipient-distributionId pair.
*/
public void deleteAllFor(@NonNull RecipientId recipientId, @NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String query = RECIPIENT_ID + " = ? AND " + DISTRIBUTION_ID + " = ?";
String[] args = SqlUtil.buildArgs(recipientId, distributionId);
db.delete(TABLE_NAME, query, args);
}
/**
* Deletes all database state.
*/
public void deleteAll() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
}

View File

@@ -0,0 +1,141 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* Keeps track of which recipients are aware of which distributionIds. For the storage of sender
* keys themselves, see {@link SenderKeyDatabase}.
*/
public class SenderKeySharedDatabase extends Database {
private static final String TAG = Log.tag(SenderKeySharedDatabase.class);
public static final String TABLE_NAME = "sender_key_shared";
private static final String ID = "_id";
public static final String DISTRIBUTION_ID = "distribution_id";
public static final String ADDRESS = "address";
public static final String DEVICE = "device";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
DISTRIBUTION_ID + " TEXT NOT NULL, " +
ADDRESS + " TEXT NOT NULL, " +
DEVICE + " INTEGER NOT NULL, " +
"UNIQUE(" + DISTRIBUTION_ID + "," + ADDRESS + ", " + DEVICE + ") ON CONFLICT REPLACE);";
SenderKeySharedDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
/**
* Mark that a distributionId has been shared with the provided recipients
*/
public void markAsShared(@NonNull DistributionId distributionId, @NonNull Collection<SignalProtocolAddress> addresses) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (SignalProtocolAddress address : addresses) {
ContentValues values = new ContentValues();
values.put(ADDRESS, address.getName());
values.put(DEVICE, address.getDeviceId());
values.put(DISTRIBUTION_ID, distributionId.toString());
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Get the set of recipientIds that know about the distributionId in question.
*/
public @NonNull Set<SignalProtocolAddress> getSharedWith(@NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = DISTRIBUTION_ID + " = ?";
String[] args = SqlUtil.buildArgs(distributionId);
Set<SignalProtocolAddress> addresses = new HashSet<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ADDRESS, DEVICE }, query, args, null, null, null)) {
while (cursor.moveToNext()) {
String address = CursorUtil.requireString(cursor, ADDRESS);
int device = CursorUtil.requireInt(cursor, DEVICE);
addresses.add(new SignalProtocolAddress(address, device));
}
}
return addresses;
}
/**
* Clear the shared statuses for all provided addresses.
*/
public void delete(@NonNull DistributionId distributionId, @NonNull Collection<SignalProtocolAddress> addresses) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String query = DISTRIBUTION_ID + " = ? AND " + ADDRESS + " = ? AND " + DEVICE + " = ?";
db.beginTransaction();
try {
for (SignalProtocolAddress address : addresses) {
db.delete(TABLE_NAME, query, SqlUtil.buildArgs(distributionId, address.getName(), address.getDeviceId()));
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Clear all shared statuses for a given distributionId.
*/
public void deleteAllFor(@NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, DISTRIBUTION_ID + " = ?", SqlUtil.buildArgs(distributionId));
}
/**
* Clear all shared statuses for a given recipientId.
*/
public void deleteAllFor(@NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasUuid()) {
db.delete(TABLE_NAME, ADDRESS + " = ?", SqlUtil.buildArgs(recipient.getUuid().get().toString()));
} else {
Log.w(TAG, "Recipient doesn't have a UUID! " + recipientId);
}
}
/**
* Clears all database content.
*/
public void deleteAll() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
}

View File

@@ -11,13 +11,13 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -72,6 +72,36 @@ public class SessionDatabase extends Database {
return null;
}
public @NonNull List<SessionRecord> load(@NonNull List<RecipientDevice> ids) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<SessionRecord> sessions = new ArrayList<>(ids.size());
database.beginTransaction();
try {
String[] projection = new String[]{RECORD};
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ?";
for (RecipientDevice id : ids) {
String[] args = SqlUtil.buildArgs(id.getRecipientId(), id.getDevice());
try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
try {
sessions.add(new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD))));
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
return sessions;
}
public @NonNull List<SessionRow> getAllFor(@NonNull RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<SessionRow> results = new LinkedList<>();
@@ -180,4 +210,22 @@ public class SessionDatabase extends Database {
return record;
}
}
public static final class RecipientDevice {
private final RecipientId recipientId;
private final int device;
public RecipientDevice(@NonNull RecipientId recipientId, int device) {
this.recipientId = recipientId;
this.device = device;
}
public @NonNull RecipientId getRecipientId() {
return recipientId;
}
public int getDevice() {
return device;
}
}
}

View File

@@ -1156,7 +1156,7 @@ public class SmsDatabase extends MessageDatabase {
}
@Override
public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.resolved(recipientId));
long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT;
@@ -1185,6 +1185,28 @@ public class SmsDatabase extends MessageDatabase {
return new InsertResult(messageId, threadId);
}
@Override
public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(ADDRESS_DEVICE_ID, senderDevice);
values.put(DATE_SENT, sentTimestamp);
values.put(DATE_RECEIVED, receivedTimestamp);
values.put(DATE_SERVER, -1);
values.put(READ, 0);
values.put(TYPE, Types.BAD_DECRYPT_TYPE);
values.put(THREAD_ID, threadId);
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
}
@Override
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
boolean forceSms, long date, InsertListener insertListener)

View File

@@ -40,10 +40,13 @@ import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PaymentDatabase;
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
@@ -73,6 +76,7 @@ import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Triple;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.push.DistributionId;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -192,8 +196,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int CHAT_COLORS = 100;
private static final int AVATAR_COLORS = 101;
private static final int EMOJI_SEARCH = 102;
private static final int SENDER_KEY = 103;
private static final int DATABASE_VERSION = 102;
private static final int DATABASE_VERSION = 103;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -221,6 +226,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE);
db.execSQL(SignedPreKeyDatabase.CREATE_TABLE);
db.execSQL(SessionDatabase.CREATE_TABLE);
db.execSQL(SenderKeyDatabase.CREATE_TABLE);
db.execSQL(SenderKeySharedDatabase.CREATE_TABLE);
db.execSQL(PendingRetryReceiptDatabase.CREATE_TABLE);
db.execSQL(StickerDatabase.CREATE_TABLE);
db.execSQL(UnknownStorageIdDatabase.CREATE_TABLE);
db.execSQL(MentionDatabase.CREATE_TABLE);
@@ -1513,6 +1521,44 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL("CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED)");
}
if (oldVersion < SENDER_KEY && !SqlUtil.tableExists(db, "sender_keys")) {
db.execSQL("CREATE TABLE sender_keys (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"recipient_id INTEGER NOT NULL, " +
"device INTEGER NOT NULL, " +
"distribution_id TEXT NOT NULL, " +
"record BLOB NOT NULL, " +
"created_at INTEGER NOT NULL, " +
"UNIQUE(recipient_id, device, distribution_id) ON CONFLICT REPLACE)");
db.execSQL("CREATE TABLE sender_key_shared (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"distribution_id TEXT NOT NULL, " +
"address TEXT NOT NULL, " +
"device INTEGER NOT NULL, " +
"UNIQUE(distribution_id, address, device) ON CONFLICT REPLACE)");
db.execSQL("CREATE TABLE pending_retry_receipts (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"author TEXT NOT NULL, " +
"device INTEGER NOT NULL, " +
"sent_timestamp INTEGER NOT NULL, " +
"received_timestamp TEXT NOT NULL, " +
"thread_id INTEGER NOT NULL, " +
"UNIQUE(author, sent_timestamp) ON CONFLICT REPLACE);");
db.execSQL("ALTER TABLE groups ADD COLUMN distribution_id TEXT DEFAULT NULL");
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON groups (distribution_id)");
try (Cursor cursor = db.query("groups", new String[] { "group_id" }, "LENGTH(group_id) = 85", null, null, null, null)) {
while (cursor.moveToNext()) {
String groupId = cursor.getString(cursor.getColumnIndexOrThrow("group_id"));
ContentValues values = new ContentValues();
values.put("distribution_id", DistributionId.create().toString());
db.update("groups", values, "group_id = ?", new String[] { groupId });
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -103,7 +103,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
if (MmsDatabase.Types.isFailedDecryptType(type)) {
if (MmsDatabase.Types.isChatSessionRefresh(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
} else if (MmsDatabase.Types.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));

View File

@@ -57,7 +57,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -191,8 +190,10 @@ public abstract class MessageRecord extends DisplayRecord {
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_16);
} else if (isGroupV1MigrationEvent()) {
return getGroupMigrationEventDescription(context);
} else if (isFailedDecryptionType()) {
} else if (isChatSessionRefresh()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_chat_session_refreshed), R.drawable.ic_refresh_16);
} else if (isBadDecryptType()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_a_message_from_s_couldnt_be_delivered, r.getDisplayName(context)), R.drawable.ic_error_outline_14);
}
return null;
@@ -458,6 +459,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isCorruptedKeyExchange(type);
}
public boolean isBadDecryptType() {
return MmsSmsColumns.Types.isBadDecryptType(type);
}
public boolean isInvalidVersionKeyExchange() {
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
}
@@ -476,8 +481,8 @@ public abstract class MessageRecord extends DisplayRecord {
public boolean isUpdate() {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isFailedDecryptionType();
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType();
}
public boolean isMediaPending() {
@@ -512,8 +517,8 @@ public abstract class MessageRecord extends DisplayRecord {
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
}
public boolean isFailedDecryptionType() {
return MmsSmsColumns.Types.isFailedDecryptType(type);
public boolean isChatSessionRefresh() {
return MmsSmsColumns.Types.isChatSessionRefresh(type);
}
public boolean isInMemoryMessageRecord() {

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.database.model
import org.thoughtcrime.securesms.recipients.RecipientId
/** A model for [org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase] */
data class PendingRetryReceiptModel(
val id: Long,
val author: RecipientId,
val authorDevice: Int,
val sentTimestamp: Long,
val receivedTimestamp: Long,
val threadId: Long
)

View File

@@ -65,7 +65,7 @@ public class SmsMessageRecord extends MessageRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
if (SmsDatabase.Types.isFailedDecryptType(type)) {
if (SmsDatabase.Types.isChatSessionRefresh(type)) {
return emphasisAdded(context.getString(R.string.MessageRecord_chat_session_refreshed));
} else if (isCorruptedKeyExchange()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message));