Add upload/download size restrictions for attachments based on remote config.

This commit is contained in:
Clark 2023-06-30 11:06:36 -04:00 committed by Greyson Parrelli
parent 87d4dba32b
commit f4a082584c
10 changed files with 90 additions and 45 deletions

View file

@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.s3.S3;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; 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); 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_MESSAGE_ID = "message_id";
private static final String KEY_PART_ROW_ID = "part_row_id"; private static final String KEY_PART_ROW_ID = "part_row_id";
private static final String KEY_PAR_UNIQUE_ID = "part_unique_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) final Attachment attachment)
throws IOException, RetryLaterException throws IOException, RetryLaterException
{ {
long maxReceiveSize = FeatureFlags.maxAttachmentReceiveSizeBytes();
AttachmentTable database = SignalDatabase.attachments(); AttachmentTable database = SignalDatabase.attachments();
File attachmentFile = database.getOrCreateTransferFile(attachmentId); File attachmentFile = database.getOrCreateTransferFile(attachmentId);
try { try {
if (attachment.getSize() > maxReceiveSize) {
throw new MmsException("Attachment too large, failing download");
}
SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment); SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
InputStream stream = messageReceiver.retrieveAttachment(pointer, InputStream stream = messageReceiver.retrieveAttachment(pointer,
attachmentFile, attachmentFile,
ByteUnit.MEGABYTES.toBytes(FeatureFlags.maxAttachmentSizeMb()) + MAX_ATTACHMENT_SIZE_BUFFER, maxReceiveSize,
(total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress))); (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)));
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream); database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
@ -269,6 +269,9 @@ public final class AttachmentDownloadJob extends BaseJob {
try (Response response = S3.getObject(Objects.requireNonNull(attachment.getFileName()))) { try (Response response = S3.getObject(Objects.requireNonNull(attachment.getFileName()))) {
ResponseBody body = response.body(); ResponseBody body = response.body();
if (body != null) { 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()); SignalDatabase.attachments().insertAttachmentsForPlaceholder(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
} }
} catch (MmsException e) { } catch (MmsException e) {

View file

@ -26,13 +26,16 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.NotificationController; import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; 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.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException; import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.IOException; import java.io.IOException;
@ -64,6 +67,12 @@ public final class AttachmentUploadJob extends BaseJob {
*/ */
private static final int FOREGROUND_LIMIT = 10 * 1024 * 1024; 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 final AttachmentId attachmentId;
private boolean forceV2; private boolean forceV2;

View file

@ -11,11 +11,14 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -50,31 +53,39 @@ public abstract class MediaConstraints {
*/ */
public abstract int[] getImageDimensionTargets(Context context); public abstract int[] getImageDimensionTargets(Context context);
public abstract int getGifMaxSize(Context context); public abstract long getGifMaxSize(Context context);
public abstract int getVideoMaxSize(Context context); public abstract long getVideoMaxSize(Context context);
public @IntRange(from = 0, to = 100) int getImageCompressionQualitySetting(@NonNull Context context) { public @IntRange(from = 0, to = 100) int getImageCompressionQualitySetting(@NonNull Context context) {
return 70; return 70;
} }
public int getUncompressedVideoMaxSize(Context context) { public long getUncompressedVideoMaxSize(Context context) {
return getVideoMaxSize(context); return getVideoMaxSize(context);
} }
public int getCompressedVideoMaxSize(Context context) { public long getCompressedVideoMaxSize(Context context) {
return getVideoMaxSize(context); return getVideoMaxSize(context);
} }
public abstract int getAudioMaxSize(Context context); public abstract long getAudioMaxSize(Context context);
public abstract int getDocumentMaxSize(Context context); public abstract long getDocumentMaxSize(Context context);
public long getMaxAttachmentSize() {
return AttachmentUploadJob.getMaxPlaintextSize();
}
public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) { public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) {
try { try {
return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize(context) && isWithinBounds(context, attachment.getUri())) || long size = attachment.getSize();
(MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize(context) && isWithinBounds(context, attachment.getUri())) || if (size > getMaxAttachmentSize()) {
(MediaUtil.isAudio(attachment) && attachment.getSize() <= getAudioMaxSize(context)) || return false;
(MediaUtil.isVideo(attachment) && attachment.getSize() <= getVideoMaxSize(context)) || }
(MediaUtil.isFile(attachment) && attachment.getSize() <= getDocumentMaxSize(context)); 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) { } catch (IOException ioe) {
Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe); Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe);
return false; return false;
@ -83,6 +94,9 @@ public abstract class MediaConstraints {
public boolean isSatisfied(@NonNull Context context, @NonNull Uri uri, @NonNull String contentType, long size) { public boolean isSatisfied(@NonNull Context context, @NonNull Uri uri, @NonNull String contentType, long size) {
try { try {
if (size > getMaxAttachmentSize()) {
return false;
}
return (MediaUtil.isGif(contentType) && size <= getGifMaxSize(context) && isWithinBounds(context, uri)) || return (MediaUtil.isGif(contentType) && size <= getGifMaxSize(context) && isWithinBounds(context, uri)) ||
(MediaUtil.isImageType(contentType) && size <= getImageMaxSize(context) && isWithinBounds(context, uri)) || (MediaUtil.isImageType(contentType) && size <= getImageMaxSize(context) && isWithinBounds(context, uri)) ||
(MediaUtil.isAudioType(contentType) && size <= getAudioMaxSize(context)) || (MediaUtil.isAudioType(contentType) && size <= getAudioMaxSize(context)) ||

View file

@ -43,27 +43,27 @@ final class MmsMediaConstraints extends MediaConstraints {
} }
@Override @Override
public int getGifMaxSize(Context context) { public long getGifMaxSize(Context context) {
return getMaxMessageSize(context); return getMaxMessageSize(context);
} }
@Override @Override
public int getVideoMaxSize(Context context) { public long getVideoMaxSize(Context context) {
return getMaxMessageSize(context); return getMaxMessageSize(context);
} }
@Override @Override
public int getUncompressedVideoMaxSize(Context context) { public long getUncompressedVideoMaxSize(Context context) {
return Math.max(getVideoMaxSize(context), 15 * 1024 * 1024); return Math.max(getVideoMaxSize(context), 15 * 1024 * 1024);
} }
@Override @Override
public int getAudioMaxSize(Context context) { public long getAudioMaxSize(Context context) {
return getMaxMessageSize(context); return getMaxMessageSize(context);
} }
@Override @Override
public int getDocumentMaxSize(Context context) { public long getDocumentMaxSize(Context context) {
return getMaxMessageSize(context); return getMaxMessageSize(context);
} }

View file

@ -40,7 +40,7 @@ public class PushMediaConstraints extends MediaConstraints {
@Override @Override
public int getImageMaxSize(Context context) { public int getImageMaxSize(Context context) {
return currentConfig.maxImageFileSize; return (int) Math.min(currentConfig.maxImageFileSize, getMaxAttachmentSize());
} }
@Override @Override
@ -49,35 +49,35 @@ public class PushMediaConstraints extends MediaConstraints {
} }
@Override @Override
public int getGifMaxSize(Context context) { public long getGifMaxSize(Context context) {
return 25 * MB; return Math.min(25 * MB, getMaxAttachmentSize());
} }
@Override @Override
public int getVideoMaxSize(Context context) { public long getVideoMaxSize(Context context) {
return 100 * MB; return getMaxAttachmentSize();
} }
@Override @Override
public int getUncompressedVideoMaxSize(Context context) { public long getUncompressedVideoMaxSize(Context context) {
return isVideoTranscodeAvailable() ? 500 * MB return isVideoTranscodeAvailable() ? 500 * MB
: getVideoMaxSize(context); : getVideoMaxSize(context);
} }
@Override @Override
public int getCompressedVideoMaxSize(Context context) { public long getCompressedVideoMaxSize(Context context) {
return Util.isLowMemory(context) ? 30 * MB return Util.isLowMemory(context) ? 30 * MB
: 50 * MB; : 50 * MB;
} }
@Override @Override
public int getAudioMaxSize(Context context) { public long getAudioMaxSize(Context context) {
return 100 * MB; return getMaxAttachmentSize();
} }
@Override @Override
public int getDocumentMaxSize(Context context) { public long getDocumentMaxSize(Context context) {
return 100 * MB; return getMaxAttachmentSize();
} }
@Override @Override

View file

@ -27,22 +27,22 @@ public class ProfileMediaConstraints extends MediaConstraints {
} }
@Override @Override
public int getGifMaxSize(Context context) { public long getGifMaxSize(Context context) {
return 0; return 0;
} }
@Override @Override
public int getVideoMaxSize(Context context) { public long getVideoMaxSize(Context context) {
return 0; return 0;
} }
@Override @Override
public int getAudioMaxSize(Context context) { public long getAudioMaxSize(Context context) {
return 0; return 0;
} }
@Override @Override
public int getDocumentMaxSize(Context context) { public long getDocumentMaxSize(Context context) {
return 0; return 0;
} }
} }

View file

@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob; import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; 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 AD_HOC_CALLING = "android.calling.ad.hoc.2";
private static final String EDIT_MESSAGE_SEND = "android.editMessage.send.3"; 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_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 * 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, ANY_ADDRESS_PORTS_KILL_SWITCH,
EDIT_MESSAGE_SEND, EDIT_MESSAGE_SEND,
MAX_ATTACHMENT_COUNT, MAX_ATTACHMENT_COUNT,
MAX_ATTACHMENT_SIZE_MB, MAX_ATTACHMENT_RECEIVE_SIZE_BYTES,
MAX_ATTACHMENT_SIZE_BYTES,
AD_HOC_CALLING AD_HOC_CALLING
); );
@ -231,7 +234,8 @@ public final class FeatureFlags {
CDS_HARD_LIMIT, CDS_HARD_LIMIT,
EDIT_MESSAGE_SEND, EDIT_MESSAGE_SEND,
MAX_ATTACHMENT_COUNT, 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); return getInteger(MAX_ATTACHMENT_COUNT, 32);
} }
/** Maximum attachment size, in mebibytes. */ /** Maximum attachment size for ciphertext in bytes. */
public static int maxAttachmentSizeMb() { public static long maxAttachmentReceiveSizeBytes() {
return getInteger(MAX_ATTACHMENT_SIZE_MB, 100); 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. */ /** Only for rendering debug info. */

View file

@ -41,8 +41,8 @@ public final class VideoUtil {
} }
public static int getMaxVideoRecordDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) { public static int getMaxVideoRecordDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) {
int allowedSize = mediaConstraints.getCompressedVideoMaxSize(context); long allowedSize = mediaConstraints.getCompressedVideoMaxSize(context);
int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND); int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND);
return Math.min(duration, VIDEO_MAX_RECORD_LENGTH_S); return Math.min(duration, VIDEO_MAX_RECORD_LENGTH_S);
} }

View file

@ -89,7 +89,11 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream {
} }
public static long getCiphertextLength(long plaintextLength) { 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() { private Mac initializeMac() {

View file

@ -56,4 +56,8 @@ public class PaddingInputStream extends FilterInputStream {
public static long getPaddedSize(long size) { 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))))); 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))));
}
} }