mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Initial pre-alpha support for sender key.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user