diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt index 5e741f466a..da869e7757 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt @@ -142,6 +142,7 @@ class ConversationItemPreviewer { 1024, 1024, Optional.empty(), + Optional.empty(), Optional.of("/not-there.jpg"), false, false, diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index a3ffd95ac8..d5e82aa2f1 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.registration.VerifyResponse import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponseProcessor @@ -87,7 +88,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() password = Util.getSecret(18), registrationId = registrationRepository.registrationId, profileKey = registrationRepository.getProfileKey("+15555550101"), - preKeyCollections = RegistrationRepository.generatePreKeys()!!, + aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI), + pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI), fcmToken = null, pniRegistrationId = registrationRepository.pniRegistrationId, recoveryPassword = "asdfasdfasdfasdf" diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java index 5f872042d0..7973ef911a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java @@ -35,6 +35,9 @@ public abstract class Attachment { @Nullable private final byte[] digest; + @Nullable + private final byte[] incrementalDigest; + @Nullable private final String fastPreflightId; @@ -70,6 +73,7 @@ public abstract class Attachment { @Nullable String key, @Nullable String relay, @Nullable byte[] digest, + @Nullable byte[] incrementalDigest, @Nullable String fastPreflightId, boolean voiceNote, boolean borderless, @@ -93,6 +97,7 @@ public abstract class Attachment { this.key = key; this.relay = relay; this.digest = digest; + this.incrementalDigest = incrementalDigest; this.fastPreflightId = fastPreflightId; this.voiceNote = voiceNote; this.borderless = borderless; @@ -165,6 +170,11 @@ public abstract class Attachment { return digest; } + @Nullable + public byte[] getIncrementalDigest() { + return incrementalDigest; + } + @Nullable public String getFastPreflightId() { return fastPreflightId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index 2634564c68..d54b260432 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -33,6 +33,7 @@ public class DatabaseAttachment extends Attachment { String key, String relay, byte[] digest, + byte[] incrementalDigest, String fastPreflightId, boolean voiceNote, boolean borderless, @@ -48,7 +49,7 @@ public class DatabaseAttachment extends Attachment { int displayOrder, long uploadTimestamp) { - super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties); + super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index 3135101c4f..e8774b7200 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MessageTable; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null); + super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null); } @Nullable diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java index a8e4150ebb..21cc8efd2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment { @Nullable String key, @Nullable String relay, @Nullable byte[] digest, + @Nullable byte[] incrementalDigest, @Nullable String fastPreflightId, boolean voiceNote, boolean borderless, @@ -41,7 +42,7 @@ public class PointerAttachment extends Attachment { @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash) { - super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null); + super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null); } @Nullable @@ -112,6 +113,7 @@ public class PointerAttachment extends Attachment { pointer.get().asPointer().getRemoteId().toString(), encodedKey, null, pointer.get().asPointer().getDigest().orElse(null), + pointer.get().asPointer().getincrementalDigest().orElse(null), fastPreflightId, pointer.get().asPointer().getVoiceNote(), pointer.get().asPointer().isBorderless(), @@ -137,6 +139,7 @@ public class PointerAttachment extends Attachment { thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, null, thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, + thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null, null, false, false, @@ -166,6 +169,7 @@ public class PointerAttachment extends Attachment { thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, null, thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, + thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null, null, false, false, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java index e2e2c44d41..6748b64738 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable; public class TombstoneAttachment extends Attachment { public TombstoneAttachment(@NonNull String contentType, boolean quote) { - super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null); + super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java index eb0fdad184..4cc9b99875 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -52,7 +52,7 @@ public class UriAttachment extends Attachment { @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties) { - super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); + super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); this.dataUri = Objects.requireNonNull(dataUri); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java index 42f71ab6c4..7bd7bab7b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java @@ -126,6 +126,7 @@ public class AttachmentTable extends DatabaseTable { static final String DISPLAY_ORDER = "display_order"; static final String UPLOAD_TIMESTAMP = "upload_timestamp"; static final String CDN_NUMBER = "cdn_number"; + static final String MAC_DIGEST = "incremental_mac_digest"; private static final String DIRECTORY = "parts"; @@ -143,7 +144,7 @@ public class AttachmentTable extends DatabaseTable { private static final String[] PROJECTION = new String[] {ROW_ID, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, CDN_NUMBER, CONTENT_LOCATION, DATA, - TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, + TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, MAC_DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, VIDEO_GIF, QUOTE, DATA_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH, @@ -188,7 +189,8 @@ public class AttachmentTable extends DatabaseTable { TRANSFER_FILE + " TEXT DEFAULT NULL, " + DISPLAY_ORDER + " INTEGER DEFAULT 0, " + UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0, " + - CDN_NUMBER + " INTEGER DEFAULT 0);"; + CDN_NUMBER + " INTEGER DEFAULT 0, " + + MAC_DIGEST + " BLOB);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -698,6 +700,7 @@ public class AttachmentTable extends DatabaseTable { contentValues.put(CDN_NUMBER, sourceAttachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation()); contentValues.put(DIGEST, sourceAttachment.getDigest()); + contentValues.put(MAC_DIGEST, sourceAttachment.getIncrementalDigest()); contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey()); contentValues.put(NAME, sourceAttachment.getRelay()); contentValues.put(SIZE, sourceAttachment.getSize()); @@ -746,6 +749,7 @@ public class AttachmentTable extends DatabaseTable { values.put(CDN_NUMBER, attachment.getCdnNumber()); values.put(CONTENT_LOCATION, attachment.getLocation()); values.put(DIGEST, attachment.getDigest()); + values.put(MAC_DIGEST, attachment.getIncrementalDigest()); values.put(CONTENT_DISPOSITION, attachment.getKey()); values.put(NAME, attachment.getRelay()); values.put(SIZE, attachment.getSize()); @@ -1272,6 +1276,7 @@ public class AttachmentTable extends DatabaseTable { object.getString(CONTENT_DISPOSITION), object.getString(NAME), null, + null, object.getString(FAST_PREFLIGHT_ID), object.getInt(VOICE_NOTE) == 1, object.getInt(BORDERLESS) == 1, @@ -1319,6 +1324,7 @@ public class AttachmentTable extends DatabaseTable { cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), cursor.getString(cursor.getColumnIndexOrThrow(NAME)), cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), + cursor.getBlob(cursor.getColumnIndexOrThrow(MAC_DIGEST)), cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1, @@ -1385,6 +1391,7 @@ public class AttachmentTable extends DatabaseTable { contentValues.put(CDN_NUMBER, useTemplateUpload ? template.getCdnNumber() : attachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, useTemplateUpload ? template.getLocation() : attachment.getLocation()); contentValues.put(DIGEST, useTemplateUpload ? template.getDigest() : attachment.getDigest()); + contentValues.put(MAC_DIGEST, useTemplateUpload ? template.getIncrementalDigest() : attachment.getIncrementalDigest()); contentValues.put(CONTENT_DISPOSITION, useTemplateUpload ? template.getKey() : attachment.getKey()); contentValues.put(NAME, useTemplateUpload ? template.getRelay() : attachment.getRelay()); contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(attachment.getFileName())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index 5aa7d698b1..00bcc143b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -48,6 +48,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CAPTION}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.NAME}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP}, + ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MAC_DIGEST}, ${MessageTable.TABLE_NAME}.${MessageTable.TYPE}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED}, @@ -55,7 +56,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}, ${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID}, ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID - FROM + FROM ${AttachmentTable.TABLE_NAME} LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID} LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 3adac0194c..2bf69bf450 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V194_KyberPreKeyMig import org.thoughtcrime.securesms.database.helpers.migration.V195_GroupMemberForeignKeyMigration import org.thoughtcrime.securesms.database.helpers.migration.V196_BackCallLinksWithRecipientV2 import org.thoughtcrime.securesms.database.helpers.migration.V197_DropAvatarColorFromCallLinks +import org.thoughtcrime.securesms.database.helpers.migration.V198_AddMacDigestColumn /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -61,7 +62,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 197 + const val DATABASE_VERSION = 198 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -260,6 +261,10 @@ object SignalDatabaseMigrations { if (oldVersion < 197) { V197_DropAvatarColorFromCallLinks.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 198) { + V198_AddMacDigestColumn.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt new file mode 100644 index 0000000000..ff9de352d7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * New field migration. + */ +@Suppress("ClassName") +object V198_AddMacDigestColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE part ADD COLUMN incremental_mac_digest BLOB") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index b53272f5e8..11e2009b08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -247,6 +247,7 @@ public final class AttachmentDownloadJob extends BaseJob { Optional.empty(), 0, 0, Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.getIncrementalDigest()), Optional.ofNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java index a4f6fba533..359f9fea48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java @@ -86,7 +86,7 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob { attachment.deleteOnExit(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis()); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, Optional.empty(), fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis()); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 27c08e26dd..042abd8a05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -283,6 +283,7 @@ public abstract class PushSendJob extends SendJob { width, height, Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.getIncrementalDigest()), Optional.ofNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java index 049760c631..4b2dcb6152 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java @@ -22,22 +22,24 @@ class AttachmentStreamLocalUriFetcher implements DataFetcher { private final File attachment; private final byte[] key; private final Optional digest; + private final Optional incrementalDigest; private final long plaintextLength; private InputStream is; - AttachmentStreamLocalUriFetcher(File attachment, long plaintextLength, byte[] key, Optional digest) { - this.attachment = attachment; - this.plaintextLength = plaintextLength; - this.digest = digest; - this.key = key; + AttachmentStreamLocalUriFetcher(File attachment, long plaintextLength, byte[] key, Optional digest, Optional incrementalDigest) { + this.attachment = attachment; + this.plaintextLength = plaintextLength; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.key = key; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { if (!digest.isPresent()) throw new InvalidMessageException("No attachment digest!"); - is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get()); + is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get(), incrementalDigest.get()); callback.onDataReady(is); } catch (IOException | InvalidMessageException e) { callback.onLoadFailed(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java index cdb54cb3ce..a8c3f8772d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java @@ -20,7 +20,7 @@ public class AttachmentStreamUriLoader implements ModelLoader buildLoadData(@NonNull AttachmentModel attachmentModel, int width, int height, @NonNull Options options) { - return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.plaintextLength, attachmentModel.key, attachmentModel.digest)); + return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.plaintextLength, attachmentModel.key, attachmentModel.digest, attachmentModel.incrementalDigest)); } @Override @@ -45,15 +45,20 @@ public class AttachmentStreamUriLoader implements ModelLoader digest; + public @NonNull Optional incrementalDigest; public long plaintextLength; - public AttachmentModel(@NonNull File attachment, @NonNull byte[] key, - long plaintextLength, @NonNull Optional digest) + public AttachmentModel(@NonNull File attachment, + @NonNull byte[] key, + long plaintextLength, + @NonNull Optional digest, + @NonNull Optional incrementalDigest) { - this.attachment = attachment; - this.key = key; - this.digest = digest; - this.plaintextLength = plaintextLength; + this.attachment = attachment; + this.key = key; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.plaintextLength = plaintextLength; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt b/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt index 6449b59040..20ff6b1607 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt @@ -44,6 +44,7 @@ object ReleaseChannel { mediaWidth, mediaHeight, Optional.empty(), + Optional.empty(), Optional.of(media), false, false, diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt b/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt index 5ae719ed2d..bae5ac9493 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt @@ -40,6 +40,7 @@ object FakeMessageRecords { key: String = "", relay: String = "", digest: ByteArray = byteArrayOf(), + incrementalDigest: ByteArray = byteArrayOf(), fastPreflightId: String = "", voiceNote: Boolean = false, borderless: Boolean = false, @@ -69,6 +70,7 @@ object FakeMessageRecords { key, relay, digest, + incrementalDigest, fastPreflightId, voiceNote, borderless, diff --git a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt index e98da4a115..757ce5893b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt @@ -242,6 +242,7 @@ class UploadDependencyGraphTest { attachment.key, attachment.relay, attachment.digest, + attachment.incrementalDigest, attachment.fastPreflightId, attachment.isVoiceNote, attachment.isBorderless, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 02779e6cdd..bd09f16ec9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -154,7 +154,7 @@ public class SignalServiceMessageReceiver { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener); - return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get()); + return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), pointer.getincrementalDigest().orElse(new byte[0])); } public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 10ea9f2cac..5cdecf3fdc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.util.Uint64Util; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; @@ -762,7 +763,7 @@ public class SignalServiceMessageSender { v2UploadAttributes = socket.getAttachmentV2UploadAttributes(); } - Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes); + Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes); return new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()), @@ -771,7 +772,8 @@ public class SignalServiceMessageSender { Optional.of(Util.toIntExact(attachment.getLength())), attachment.getPreview(), attachment.getWidth(), attachment.getHeight(), - Optional.of(attachmentIdAndDigest.second()), + Optional.of(attachmentIdAndDigest.second().getDigest()), + Optional.of(attachmentIdAndDigest.second().getIncrementalDigest()), attachment.getFileName(), attachment.getVoiceNote(), attachment.isBorderless(), @@ -811,7 +813,7 @@ public class SignalServiceMessageSender { } private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException { - byte[] digest = socket.uploadAttachment(attachmentData); + AttachmentDigest digest = socket.uploadAttachment(attachmentData); return new SignalServiceAttachmentPointer(attachmentData.getResumableUploadSpec().getCdnNumber(), new SignalServiceAttachmentRemoteId(attachmentData.getResumableUploadSpec().getCdnKey()), attachment.getContentType(), @@ -820,7 +822,8 @@ public class SignalServiceMessageSender { attachment.getPreview(), attachment.getWidth(), attachment.getHeight(), - Optional.of(digest), + Optional.of(digest.getDigest()), + Optional.ofNullable(digest.getIncrementalDigest()), attachment.getFileName(), attachment.getVoiceNote(), attachment.isBorderless(), diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java index b6ce5829ce..b9d853708c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java @@ -8,6 +8,8 @@ package org.whispersystems.signalservice.api.crypto; import org.signal.libsignal.protocol.InvalidMacException; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice; +import org.signal.libsignal.protocol.incrementalmac.IncrementalMacInputStream; import org.signal.libsignal.protocol.kdf.HKDFv3; import org.whispersystems.signalservice.internal.util.ContentLengthInputStream; import org.whispersystems.signalservice.internal.util.Util; @@ -51,7 +53,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { private long totalRead; private byte[] overflowBuffer; - public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest) + public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest) throws InvalidMessageException, IOException { try { @@ -71,7 +73,18 @@ public class AttachmentCipherInputStream extends FilterInputStream { verifyMac(fin, file.length(), mac, digest); } - InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength()); + final FileInputStream innerStream = new FileInputStream(file); + + boolean hasIncrementalMac = incrementalDigest != null && incrementalDigest.length > 0; + + InputStream wrap = !hasIncrementalMac ? innerStream + : new IncrementalMacInputStream( + innerStream, + parts[1], + ChunkSizeChoice.inferChunkSize(Math.max(Math.toIntExact(file.length()), 1)), + incrementalDigest); + + InputStream inputStream = new AttachmentCipherInputStream(wrap, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength()); if (plaintextLength != 0) { inputStream = new ContentLengthInputStream(inputStream, plaintextLength); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java index 6ea49c346a..47d2b9bb5b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java @@ -25,6 +25,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { private final Optional size; private final Optional preview; private final Optional digest; + private final Optional incrementalDigest; private final Optional fileName; private final boolean voiceNote; private final boolean borderless; @@ -44,6 +45,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { int width, int height, Optional digest, + Optional incrementalDigest, Optional fileName, boolean voiceNote, boolean borderless, @@ -53,21 +55,22 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { long uploadTimestamp) { super(contentType); - this.cdnNumber = cdnNumber; - this.remoteId = remoteId; - this.key = key; - this.size = size; - this.preview = preview; - this.width = width; - this.height = height; - this.digest = digest; - this.fileName = fileName; - this.voiceNote = voiceNote; - this.borderless = borderless; - this.caption = caption; - this.blurHash = blurHash; - this.uploadTimestamp = uploadTimestamp; - this.gif = gif; + this.cdnNumber = cdnNumber; + this.remoteId = remoteId; + this.key = key; + this.size = size; + this.preview = preview; + this.width = width; + this.height = height; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.fileName = fileName; + this.voiceNote = voiceNote; + this.borderless = borderless; + this.caption = caption; + this.blurHash = blurHash; + this.uploadTimestamp = uploadTimestamp; + this.gif = gif; } public int getCdnNumber() { @@ -108,6 +111,10 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { return digest; } + public Optional getincrementalDigest() { + return incrementalDigest; + } + public boolean getVoiceNote() { return voiceNote; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java index cb02be7b8f..904146e1f2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java @@ -25,6 +25,7 @@ public final class AttachmentPointerUtil { pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.empty(), pointer.getWidth(), pointer.getHeight(), pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.empty(), + pointer.hasIncrementalDigest() ? Optional.of(pointer.getIncrementalDigest().toByteArray()) : Optional.empty(), pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.empty(), (pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE)) != 0, (pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE)) != 0, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt new file mode 100644 index 0000000000..cbe51e971b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.crypto + +data class AttachmentDigest(val digest: ByteArray, val incrementalDigest: ByteArray?) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index ebf846aa99..a5b53c3615 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -112,6 +112,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalUrl; import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException; import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException; import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException; @@ -1345,7 +1346,7 @@ public class PushServiceSocket { } } - public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes) + public AttachmentDigest uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes) throws IOException { return uploadToCdn0(AVATAR_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), @@ -1358,17 +1359,17 @@ public class PushServiceSocket { null, null); } - public Pair uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes) + public Pair uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes) throws PushNetworkException, NonSuccessfulResponseCodeException { - long id = Long.parseLong(uploadAttributes.getAttachmentId()); - byte[] digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), - uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(), - uploadAttributes.getCredential(), uploadAttributes.getDate(), - uploadAttributes.getSignature(), attachment.getData(), - "application/octet-stream", attachment.getDataSize(), - attachment.getOutputStreamFactory(), attachment.getListener(), - attachment.getCancelationSignal()); + long id = Long.parseLong(uploadAttributes.getAttachmentId()); + AttachmentDigest digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), + uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(), + uploadAttributes.getCredential(), uploadAttributes.getDate(), + uploadAttributes.getSignature(), attachment.getData(), + "application/octet-stream", attachment.getDataSize(), + attachment.getOutputStreamFactory(), attachment.getListener(), + attachment.getCancelationSignal()); return new Pair<>(id, digest); } @@ -1382,7 +1383,7 @@ public class PushServiceSocket { System.currentTimeMillis() + CDN2_RESUMABLE_LINK_LIFETIME_MILLIS); } - public byte[] uploadAttachment(PushAttachmentData attachment) throws IOException { + public AttachmentDigest uploadAttachment(PushAttachmentData attachment) throws IOException { if (attachment.getResumableUploadSpec() == null || attachment.getResumableUploadSpec().getExpirationTimestamp() < System.currentTimeMillis()) { throw new ResumeLocationInvalidException(); @@ -1472,11 +1473,11 @@ public class PushServiceSocket { } } - private byte[] uploadToCdn0(String path, String acl, String key, String policy, String algorithm, - String credential, String date, String signature, - InputStream data, String contentType, long length, - OutputStreamFactory outputStreamFactory, ProgressListener progressListener, - CancelationSignal cancelationSignal) + private AttachmentDigest uploadToCdn0(String path, String acl, String key, String policy, String algorithm, + String credential, String date, String signature, + InputStream data, String contentType, long length, + OutputStreamFactory outputStreamFactory, ProgressListener progressListener, + CancelationSignal cancelationSignal) throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(0), random); @@ -1516,7 +1517,7 @@ public class PushServiceSocket { } try (Response response = call.execute()) { - if (response.isSuccessful()) return file.getTransmittedDigest(); + if (response.isSuccessful()) return file.getAttachmentDigest(); else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response); } catch (PushNetworkException | NonSuccessfulResponseCodeException e) { throw e; @@ -1577,7 +1578,7 @@ public class PushServiceSocket { } } - private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException { + private AttachmentDigest uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException { ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(2), random); OkHttpClient okHttpClient = connectionHolder.getClient() .newBuilder() @@ -1593,7 +1594,7 @@ public class PushServiceSocket { try (NowhereBufferedSink buffer = new NowhereBufferedSink()) { file.writeTo(buffer); } - return file.getTransmittedDigest(); + return file.getAttachmentDigest(); } Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, resumableUrl)) @@ -1611,7 +1612,7 @@ public class PushServiceSocket { } try (Response response = call.execute()) { - if (response.isSuccessful()) return file.getTransmittedDigest(); + if (response.isSuccessful()) return file.getAttachmentDigest(); else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response); } catch (PushNetworkException | NonSuccessfulResponseCodeException e) { throw e; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java deleted file mode 100644 index f7fda59176..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.whispersystems.signalservice.internal.push.http; - - -import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream; -import org.whispersystems.signalservice.api.crypto.DigestingOutputStream; - -import java.io.IOException; -import java.io.OutputStream; - -public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory { - - private final byte[] key; - private final byte[] iv; - - public AttachmentCipherOutputStreamFactory(byte[] key, byte[] iv) { - this.key = key; - this.iv = iv; - } - - @Override - public DigestingOutputStream createFor(OutputStream wrap) throws IOException { - return new AttachmentCipherOutputStream(key, iv, wrap); - } - -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt new file mode 100644 index 0000000000..25623ad241 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt @@ -0,0 +1,40 @@ +package org.whispersystems.signalservice.internal.push.http + +import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice +import org.signal.libsignal.protocol.incrementalmac.IncrementalMacOutputStream +import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream +import org.whispersystems.signalservice.api.crypto.DigestingOutputStream +import java.io.IOException +import java.io.OutputStream + +/** + * Creates [AttachmentCipherOutputStream] using the provided [key] and [iv]. + * + * [createFor] is straightforward, and is the legacy behavior. + * [createIncrementalFor] first wraps the stream in an [IncrementalMacOutputStream] to calculate MAC digests on chunks as the stream is written to. + * + * @property key + * @property iv + */ +class AttachmentCipherOutputStreamFactory(private val key: ByteArray, private val iv: ByteArray) : OutputStreamFactory { + companion object { + private const val AES_KEY_LENGTH = 32 + } + + @Throws(IOException::class) + override fun createFor(wrap: OutputStream): DigestingOutputStream { + return AttachmentCipherOutputStream(key, iv, wrap) + } + + @Throws(IOException::class) + fun createIncrementalFor(wrap: OutputStream?, length: Long, incrementalDigestOut: OutputStream?): DigestingOutputStream { + if (length > Int.MAX_VALUE) { + throw IllegalArgumentException("Attachment length overflows int!") + } + + val privateKey = key.sliceArray(AES_KEY_LENGTH until key.size) + val chunkSizeChoice = ChunkSizeChoice.inferChunkSize(length.toInt().coerceAtLeast(1)) + val incrementalStream = IncrementalMacOutputStream(wrap, privateKey, chunkSizeChoice, incrementalDigestOut) + return createFor(incrementalStream) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java deleted file mode 100644 index b39e29eaff..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.whispersystems.signalservice.internal.push.http; - - - -import org.whispersystems.signalservice.api.crypto.DigestingOutputStream; -import org.whispersystems.signalservice.api.crypto.SkippingOutputStream; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; -import org.whispersystems.signalservice.api.util.Preconditions; - -import java.io.IOException; -import java.io.InputStream; - -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; - -public class DigestingRequestBody extends RequestBody { - - private final InputStream inputStream; - private final OutputStreamFactory outputStreamFactory; - private final String contentType; - private final long contentLength; - private final ProgressListener progressListener; - private final CancelationSignal cancelationSignal; - private final long contentStart; - - private byte[] digest; - - public DigestingRequestBody(InputStream inputStream, - OutputStreamFactory outputStreamFactory, - String contentType, long contentLength, - ProgressListener progressListener, - CancelationSignal cancelationSignal, - long contentStart) - { - Preconditions.checkArgument(contentLength >= contentStart); - Preconditions.checkArgument(contentStart >= 0); - - this.inputStream = inputStream; - this.outputStreamFactory = outputStreamFactory; - this.contentType = contentType; - this.contentLength = contentLength; - this.progressListener = progressListener; - this.cancelationSignal = cancelationSignal; - this.contentStart = contentStart; - } - - @Override - public MediaType contentType() { - return MediaType.parse(contentType); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - DigestingOutputStream outputStream = outputStreamFactory.createFor(new SkippingOutputStream(contentStart, sink.outputStream())); - byte[] buffer = new byte[8192]; - - int read; - long total = 0; - - while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { - if (cancelationSignal != null && cancelationSignal.isCanceled()) { - throw new IOException("Canceled!"); - } - - outputStream.write(buffer, 0, read); - total += read; - - if (progressListener != null) { - progressListener.onAttachmentProgress(contentLength, total); - } - } - - outputStream.flush(); - digest = outputStream.getTransmittedDigest(); - } - - @Override - public long contentLength() { - if (contentLength > 0) return contentLength - contentStart; - else return -1; - } - - public byte[] getTransmittedDigest() { - return digest; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt new file mode 100644 index 0000000000..0eb56ef1d7 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt @@ -0,0 +1,80 @@ +package org.whispersystems.signalservice.internal.push.http + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import org.whispersystems.signalservice.api.crypto.DigestingOutputStream +import org.whispersystems.signalservice.api.crypto.SkippingOutputStream +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +/** + * This [RequestBody] encrypts the data written to it before it is sent. + */ +class DigestingRequestBody( + private val inputStream: InputStream, + private val outputStreamFactory: OutputStreamFactory, + private val contentType: String, + private val contentLength: Long, + private val progressListener: SignalServiceAttachment.ProgressListener?, + private val cancelationSignal: CancelationSignal?, + private val contentStart: Long +) : RequestBody() { + lateinit var transmittedDigest: ByteArray + private set + var incrementalDigest: ByteArray? = null + private set + + init { + require(contentLength >= contentStart) + require(contentStart >= 0) + } + + override fun contentType(): MediaType? { + return MediaType.parse(contentType) + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + val digestStream = ByteArrayOutputStream() + val inner = SkippingOutputStream(contentStart, sink.outputStream()) + val outputStream: DigestingOutputStream = if (outputStreamFactory is AttachmentCipherOutputStreamFactory) { + outputStreamFactory.createIncrementalFor(inner, contentLength, digestStream) + } else { + outputStreamFactory.createFor(inner) + } + + val buffer = ByteArray(8192) + var read: Int + var total: Long = 0 + + while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) { + if (cancelationSignal?.isCanceled == true) { + throw IOException("Canceled!") + } + outputStream.write(buffer, 0, read) + total += read.toLong() + progressListener?.onAttachmentProgress(contentLength, total) + } + + outputStream.flush() + outputStream.close() + digestStream.close() + + incrementalDigest = digestStream.toByteArray() + transmittedDigest = outputStream.transmittedDigest + } + + override fun contentLength(): Long { + return if (contentLength > 0) contentLength - contentStart else -1 + } + + fun getAttachmentDigest() = AttachmentDigest(transmittedDigest, incrementalDigest) + + companion object { + const val TAG = "DigestingRequestBody" + } +} diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 7baf8f8096..74effa322e 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -667,20 +667,21 @@ message AttachmentPointer { fixed64 cdnId = 1; string cdnKey = 15; } - optional string contentType = 2; - optional bytes key = 3; - optional uint32 size = 4; - optional bytes thumbnail = 5; - optional bytes digest = 6; - optional string fileName = 7; - optional uint32 flags = 8; - optional uint32 width = 9; - optional uint32 height = 10; - optional string caption = 11; - optional string blurHash = 12; - optional uint64 uploadTimestamp = 13; - optional uint32 cdnNumber = 14; - // Next ID: 16 + optional string contentType = 2; + optional bytes key = 3; + optional uint32 size = 4; + optional bytes thumbnail = 5; + optional bytes digest = 6; + optional bytes incrementalDigest = 16; + optional string fileName = 7; + optional uint32 flags = 8; + optional uint32 width = 9; + optional uint32 height = 10; + optional string caption = 11; + optional string blurHash = 12; + optional uint64 uploadTimestamp = 13; + optional uint32 cdnNumber = 14; + // Next ID: 17 } message GroupContext { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java index e9b4883eb7..004a1ce18d 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java @@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.crypto; import org.conscrypt.Conscrypt; import org.junit.Test; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.incrementalmac.InvalidMacException; import org.signal.libsignal.protocol.kdf.HKDFv3; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory; @@ -17,9 +18,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.Security; import java.util.Arrays; +import java.util.Random; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS; public final class AttachmentCipherTest { @@ -32,9 +35,9 @@ public final class AttachmentCipherTest { public void attachment_encryptDecrypt() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "Peter Parker".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -46,9 +49,9 @@ public final class AttachmentCipherTest { public void attachment_encryptDecryptEmpty() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -57,19 +60,19 @@ public final class AttachmentCipherTest { } @Test - public void attachment_decryptFailOnBadKey() throws IOException{ + public void attachment_decryptFailOnBadKey() throws IOException { File cipherFile = null; boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Gwen Stacy".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); - byte[] badKey = new byte[64]; + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Gwen Stacy".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badKey = new byte[64]; cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -82,19 +85,19 @@ public final class AttachmentCipherTest { } @Test - public void attachment_decryptFailOnBadDigest() throws IOException{ + public void attachment_decryptFailOnBadDigest() throws IOException { File cipherFile = null; boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Mary Jane Watson".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); - byte[] badDigest = new byte[32]; + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Mary Jane Watson".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badDigest = new byte[32]; cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -106,9 +109,42 @@ public final class AttachmentCipherTest { assertTrue(hitCorrectException); } + @Test + public void attachment_decryptFailOnBadIncrementalDigest() throws IOException { + File cipherFile = null; + boolean hitCorrectException = false; + + try { + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = new byte[1000000]; + + new Random().nextBytes(plaintextInput); + + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.length); + + cipherFile = writeToFile(encryptResult.ciphertext); + + + InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, badDigest); + byte[] plaintextOutput = readInputStreamFully(decryptedStream); + fail(); + } catch (InvalidMacException e) { + hitCorrectException = true; + } catch (InvalidMessageException e) { + hitCorrectException = false; + } finally { + if (cipherFile != null) { + cipherFile.delete(); + } + } + + assertTrue(hitCorrectException); + } + @Test public void attachment_encryptDecryptPaddedContent() throws IOException, InvalidMessageException { - int[] lengths = { 531, 600, 724, 1019, 1024 }; + int[] lengths = { 531, 600, 724, 1019, 1024 }; for (int length : lengths) { byte[] plaintextInput = new byte[length]; @@ -117,24 +153,26 @@ public final class AttachmentCipherTest { plaintextInput[i] = (byte) 0x97; } - byte[] key = Util.getSecretBytes(64); - ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput); - InputStream dataStream = new PaddingInputStream(inputStream, length); - ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream(); - DigestingOutputStream digestStream = new AttachmentCipherOutputStreamFactory(key, null).createFor(encryptedStream); + byte[] key = Util.getSecretBytes(64); + byte[] iv = Util.getSecretBytes(16); + ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput); + InputStream paddedInputStream = new PaddingInputStream(inputStream, length); + ByteArrayOutputStream destinationOutputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream incrementalDigestOutputStream = new ByteArrayOutputStream(); + DigestingOutputStream encryptingOutputStream = new AttachmentCipherOutputStreamFactory(key, iv).createIncrementalFor(destinationOutputStream, length, incrementalDigestOutputStream); - Util.copy(dataStream, digestStream); - digestStream.flush(); + Util.copy(paddedInputStream, encryptingOutputStream); - byte[] digest = digestStream.getTransmittedDigest(); - byte[] encryptedData = encryptedStream.toByteArray(); + encryptingOutputStream.flush(); + encryptingOutputStream.close(); - encryptedStream.close(); - inputStream.close(); + byte[] encryptedData = destinationOutputStream.toByteArray(); + byte[] digest = encryptingOutputStream.getTransmittedDigest(); + byte[] incrementalDigest = incrementalDigestOutputStream.toByteArray(); File cipherFile = writeToFile(encryptedData); - InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest); + InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest, incrementalDigest); byte[] plaintextOutput = readInputStreamFully(decryptedStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -149,13 +187,13 @@ public final class AttachmentCipherTest { boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Aunt May".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Aunt May".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -175,14 +213,14 @@ public final class AttachmentCipherTest { try { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "Uncle Ben".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length); badMacCiphertext[badMacCiphertext.length - 1] += 1; cipherFile = writeToFile(badMacCiphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -200,7 +238,7 @@ public final class AttachmentCipherTest { byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "Peter Parker".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey); byte[] plaintextOutput = readInputStreamFully(inputStream); @@ -213,7 +251,7 @@ public final class AttachmentCipherTest { byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey); byte[] plaintextOutput = readInputStreamFully(inputStream); @@ -227,10 +265,10 @@ public final class AttachmentCipherTest { boolean hitCorrectException = false; try { - byte[] packKey = Util.getSecretBytes(32); - byte[] plaintextInput = "Gwen Stacy".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); - byte[] badPackKey = new byte[32]; + byte[] packKey = Util.getSecretBytes(32); + byte[] plaintextInput = "Gwen Stacy".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); + byte[] badPackKey = new byte[32]; AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey); } catch (InvalidMessageException e) { @@ -249,7 +287,7 @@ public final class AttachmentCipherTest { try { byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "Uncle Ben".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length); badMacCiphertext[badMacCiphertext.length - 1] += 1; @@ -262,15 +300,26 @@ public final class AttachmentCipherTest { assertTrue(hitCorrectException); } - private static EncryptResult encryptData(byte[] data, byte[] keyMaterial) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - AttachmentCipherOutputStream encryptStream = new AttachmentCipherOutputStream(keyMaterial, null, outputStream); + private static EncryptResult encryptData(byte[] data, byte[] keyMaterial, boolean withIncremental) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream incrementalDigestOut = new ByteArrayOutputStream(); + byte[] iv = Util.getSecretBytes(16); + AttachmentCipherOutputStreamFactory factory = new AttachmentCipherOutputStreamFactory(keyMaterial, iv); + + DigestingOutputStream encryptStream; + if (withIncremental) { + encryptStream = factory.createIncrementalFor(outputStream, data.length, incrementalDigestOut); + } else { + encryptStream = factory.createFor(outputStream); + } + encryptStream.write(data); encryptStream.flush(); encryptStream.close(); + incrementalDigestOut.close(); - return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest()); + return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest(), incrementalDigestOut.toByteArray()); } private static File writeToFile(byte[] data) throws IOException { @@ -296,10 +345,12 @@ public final class AttachmentCipherTest { private static class EncryptResult { final byte[] ciphertext; final byte[] digest; + final byte[] incrementalDigest; - private EncryptResult(byte[] ciphertext, byte[] digest) { + private EncryptResult(byte[] ciphertext, byte[] digest, byte[] incrementalDigest) { this.ciphertext = ciphertext; this.digest = digest; + this.incrementalDigest = incrementalDigest; } } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java index 189e4e7215..30d8e3a335 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java @@ -23,7 +23,7 @@ public class DigestingRequestBodyTest { private final OutputStreamFactory outputStreamFactory = new AttachmentCipherOutputStreamFactory(attachmentKey, attachmentIV); @Test - public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameTransmittedDigest() throws Exception { + public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameDigests() throws Exception { DigestingRequestBody fromStart = getBody(0); DigestingRequestBody fromMiddle = getBody(CONTENT_LENGTH / 2); @@ -36,6 +36,7 @@ public class DigestingRequestBodyTest { } assertArrayEquals(fromStart.getTransmittedDigest(), fromMiddle.getTransmittedDigest()); + assertArrayEquals(fromStart.getIncrementalDigest(), fromMiddle.getIncrementalDigest()); } @Test