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 11e2009b08..62074097c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.s3.S3; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.ByteUnit; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; @@ -58,9 +57,6 @@ public final class AttachmentDownloadJob extends BaseJob { private static final String TAG = Log.tag(AttachmentDownloadJob.class); - /** A little extra allowed size to account for any adjustments made by other clients */ - private static final int MAX_ATTACHMENT_SIZE_BUFFER = 25 * 1024 * 1024; - private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_PART_ROW_ID = "part_row_id"; private static final String KEY_PAR_UNIQUE_ID = "part_unique_id"; @@ -188,16 +184,20 @@ public final class AttachmentDownloadJob extends BaseJob { final Attachment attachment) throws IOException, RetryLaterException { + long maxReceiveSize = FeatureFlags.maxAttachmentReceiveSizeBytes(); AttachmentTable database = SignalDatabase.attachments(); File attachmentFile = database.getOrCreateTransferFile(attachmentId); try { + if (attachment.getSize() > maxReceiveSize) { + throw new MmsException("Attachment too large, failing download"); + } SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment); InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, - ByteUnit.MEGABYTES.toBytes(FeatureFlags.maxAttachmentSizeMb()) + MAX_ATTACHMENT_SIZE_BUFFER, + maxReceiveSize, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress))); database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream); @@ -269,6 +269,9 @@ public final class AttachmentDownloadJob extends BaseJob { try (Response response = S3.getObject(Objects.requireNonNull(attachment.getFileName()))) { ResponseBody body = response.body(); if (body != null) { + if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes()) { + throw new MmsException("Attachment too large, failing download"); + } SignalDatabase.attachments().insertAttachmentsForPlaceholder(messageId, attachmentId, Okio.buffer(body.source()).inputStream()); } } catch (MmsException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index 6a5f0c1b22..6e69169385 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -26,13 +26,16 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.NotificationController; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.IOException; @@ -64,6 +67,12 @@ public final class AttachmentUploadJob extends BaseJob { */ private static final int FOREGROUND_LIMIT = 10 * 1024 * 1024; + public static long getMaxPlaintextSize() { + long maxCipherTextSize = FeatureFlags.maxAttachmentSizeBytes(); + long maxPaddedSize = AttachmentCipherOutputStream.getPlaintextLength(maxCipherTextSize); + return PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize); + } + private final AttachmentId attachmentId; private boolean forceV2; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java index 995d1705d4..7748a39aa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -11,11 +11,14 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.jobs.AttachmentUploadJob; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import java.io.IOException; import java.io.InputStream; @@ -50,31 +53,39 @@ public abstract class MediaConstraints { */ public abstract int[] getImageDimensionTargets(Context context); - public abstract int getGifMaxSize(Context context); - public abstract int getVideoMaxSize(Context context); + public abstract long getGifMaxSize(Context context); + public abstract long getVideoMaxSize(Context context); public @IntRange(from = 0, to = 100) int getImageCompressionQualitySetting(@NonNull Context context) { return 70; } - public int getUncompressedVideoMaxSize(Context context) { + public long getUncompressedVideoMaxSize(Context context) { return getVideoMaxSize(context); } - public int getCompressedVideoMaxSize(Context context) { + public long getCompressedVideoMaxSize(Context context) { return getVideoMaxSize(context); } - public abstract int getAudioMaxSize(Context context); - public abstract int getDocumentMaxSize(Context context); + public abstract long getAudioMaxSize(Context context); + public abstract long getDocumentMaxSize(Context context); + + public long getMaxAttachmentSize() { + return AttachmentUploadJob.getMaxPlaintextSize(); + } public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) { try { - return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize(context) && isWithinBounds(context, attachment.getUri())) || - (MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize(context) && isWithinBounds(context, attachment.getUri())) || - (MediaUtil.isAudio(attachment) && attachment.getSize() <= getAudioMaxSize(context)) || - (MediaUtil.isVideo(attachment) && attachment.getSize() <= getVideoMaxSize(context)) || - (MediaUtil.isFile(attachment) && attachment.getSize() <= getDocumentMaxSize(context)); + long size = attachment.getSize(); + if (size > getMaxAttachmentSize()) { + return false; + } + return (MediaUtil.isGif(attachment) && size <= getGifMaxSize(context) && isWithinBounds(context, attachment.getUri())) || + (MediaUtil.isImage(attachment) && size <= getImageMaxSize(context) && isWithinBounds(context, attachment.getUri())) || + (MediaUtil.isAudio(attachment) && size <= getAudioMaxSize(context)) || + (MediaUtil.isVideo(attachment) && size <= getVideoMaxSize(context)) || + (MediaUtil.isFile(attachment) && size <= getDocumentMaxSize(context)); } catch (IOException ioe) { Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe); return false; @@ -83,6 +94,9 @@ public abstract class MediaConstraints { public boolean isSatisfied(@NonNull Context context, @NonNull Uri uri, @NonNull String contentType, long size) { try { + if (size > getMaxAttachmentSize()) { + return false; + } return (MediaUtil.isGif(contentType) && size <= getGifMaxSize(context) && isWithinBounds(context, uri)) || (MediaUtil.isImageType(contentType) && size <= getImageMaxSize(context) && isWithinBounds(context, uri)) || (MediaUtil.isAudioType(contentType) && size <= getAudioMaxSize(context)) || diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MmsMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MmsMediaConstraints.java index 7b3b3bb08e..dfb119ca59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/MmsMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MmsMediaConstraints.java @@ -43,27 +43,27 @@ final class MmsMediaConstraints extends MediaConstraints { } @Override - public int getGifMaxSize(Context context) { + public long getGifMaxSize(Context context) { return getMaxMessageSize(context); } @Override - public int getVideoMaxSize(Context context) { + public long getVideoMaxSize(Context context) { return getMaxMessageSize(context); } @Override - public int getUncompressedVideoMaxSize(Context context) { + public long getUncompressedVideoMaxSize(Context context) { return Math.max(getVideoMaxSize(context), 15 * 1024 * 1024); } @Override - public int getAudioMaxSize(Context context) { + public long getAudioMaxSize(Context context) { return getMaxMessageSize(context); } @Override - public int getDocumentMaxSize(Context context) { + public long getDocumentMaxSize(Context context) { return getMaxMessageSize(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 973fb2b5bd..b7ba51ed73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -40,7 +40,7 @@ public class PushMediaConstraints extends MediaConstraints { @Override public int getImageMaxSize(Context context) { - return currentConfig.maxImageFileSize; + return (int) Math.min(currentConfig.maxImageFileSize, getMaxAttachmentSize()); } @Override @@ -49,35 +49,35 @@ public class PushMediaConstraints extends MediaConstraints { } @Override - public int getGifMaxSize(Context context) { - return 25 * MB; + public long getGifMaxSize(Context context) { + return Math.min(25 * MB, getMaxAttachmentSize()); } @Override - public int getVideoMaxSize(Context context) { - return 100 * MB; + public long getVideoMaxSize(Context context) { + return getMaxAttachmentSize(); } @Override - public int getUncompressedVideoMaxSize(Context context) { + public long getUncompressedVideoMaxSize(Context context) { return isVideoTranscodeAvailable() ? 500 * MB : getVideoMaxSize(context); } @Override - public int getCompressedVideoMaxSize(Context context) { + public long getCompressedVideoMaxSize(Context context) { return Util.isLowMemory(context) ? 30 * MB : 50 * MB; } @Override - public int getAudioMaxSize(Context context) { - return 100 * MB; + public long getAudioMaxSize(Context context) { + return getMaxAttachmentSize(); } @Override - public int getDocumentMaxSize(Context context) { - return 100 * MB; + public long getDocumentMaxSize(Context context) { + return getMaxAttachmentSize(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileMediaConstraints.java index c0a0fd9eb9..352b5220b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileMediaConstraints.java @@ -27,22 +27,22 @@ public class ProfileMediaConstraints extends MediaConstraints { } @Override - public int getGifMaxSize(Context context) { + public long getGifMaxSize(Context context) { return 0; } @Override - public int getVideoMaxSize(Context context) { + public long getVideoMaxSize(Context context) { return 0; } @Override - public int getAudioMaxSize(Context context) { + public long getAudioMaxSize(Context context) { return 0; } @Override - public int getDocumentMaxSize(Context context) { + public long getDocumentMaxSize(Context context) { return 0; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 7acd138a1a..a0c051a03c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import java.io.IOException; import java.util.HashMap; @@ -106,7 +107,8 @@ public final class FeatureFlags { private static final String AD_HOC_CALLING = "android.calling.ad.hoc.2"; private static final String EDIT_MESSAGE_SEND = "android.editMessage.send.3"; private static final String MAX_ATTACHMENT_COUNT = "android.attachments.maxCount"; - private static final String MAX_ATTACHMENT_SIZE_MB = "android.attachments.maxSize"; + private static final String MAX_ATTACHMENT_RECEIVE_SIZE_BYTES = "global.attachments.maxReceiveBytes"; + private static final String MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -165,7 +167,8 @@ public final class FeatureFlags { ANY_ADDRESS_PORTS_KILL_SWITCH, EDIT_MESSAGE_SEND, MAX_ATTACHMENT_COUNT, - MAX_ATTACHMENT_SIZE_MB, + MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, + MAX_ATTACHMENT_SIZE_BYTES, AD_HOC_CALLING ); @@ -231,7 +234,8 @@ public final class FeatureFlags { CDS_HARD_LIMIT, EDIT_MESSAGE_SEND, MAX_ATTACHMENT_COUNT, - MAX_ATTACHMENT_SIZE_MB + MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, + MAX_ATTACHMENT_SIZE_BYTES ); /** @@ -594,9 +598,16 @@ public final class FeatureFlags { return getInteger(MAX_ATTACHMENT_COUNT, 32); } - /** Maximum attachment size, in mebibytes. */ - public static int maxAttachmentSizeMb() { - return getInteger(MAX_ATTACHMENT_SIZE_MB, 100); + /** Maximum attachment size for ciphertext in bytes. */ + public static long maxAttachmentReceiveSizeBytes() { + long maxAttachmentSize = maxAttachmentSizeBytes(); + long maxReceiveSize = getLong(MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, (int) (maxAttachmentSize * 1.25)); + return Math.max(maxAttachmentSize, maxReceiveSize); + } + + /** Maximum attachment ciphertext size when sending in bytes */ + public static long maxAttachmentSizeBytes() { + return getLong(MAX_ATTACHMENT_SIZE_BYTES, ByteUnit.MEGABYTES.toBytes(100)); } /** Only for rendering debug info. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java index 3689ab12e3..ea8eaa114d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java @@ -41,8 +41,8 @@ public final class VideoUtil { } public static int getMaxVideoRecordDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) { - int allowedSize = mediaConstraints.getCompressedVideoMaxSize(context); - int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND); + long allowedSize = mediaConstraints.getCompressedVideoMaxSize(context); + int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND); return Math.min(duration, VIDEO_MAX_RECORD_LENGTH_S); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherOutputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherOutputStream.java index 5c9804da4b..a352369290 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherOutputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherOutputStream.java @@ -89,7 +89,11 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream { } public static long getCiphertextLength(long plaintextLength) { - return 16 + (((plaintextLength / 16) +1) * 16) + 32; + return 16 + (((plaintextLength / 16) + 1) * 16) + 32; + } + + public static long getPlaintextLength(long ciphertextLength) { + return (((ciphertextLength - 16 - 32) / 16) - 1) * 16; } private Mac initializeMac() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java index 2833bca3e0..cf4e4bee2b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java @@ -56,4 +56,8 @@ public class PaddingInputStream extends FilterInputStream { public static long getPaddedSize(long size) { return (int) Math.max(541, Math.floor(Math.pow(1.05, Math.ceil(Math.log(size) / Math.log(1.05))))); } + + public static long getMaxUnpaddedSize(long maxPaddedSize) { + return (int) Math.floor(Math.pow(1.05, Math.floor(Math.log(maxPaddedSize) / Math.log(1.05)))); + } }