Support for receiving arbitrary attachment types

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2017-03-28 12:05:30 -07:00
parent c69efbffd2
commit f67eb5f9f3
60 changed files with 1251 additions and 423 deletions

View File

@@ -70,7 +70,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
@Override
public void onRun(MasterSecret masterSecret) throws IOException {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(attachmentId);
final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, attachmentId);
if (attachment == null) {
Log.w(TAG, "attachment no longer exists.");
@@ -158,7 +158,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
Log.w(TAG, "Downloading attachment with no digest...");
}
return new SignalServiceAttachmentPointer(id, null, key, relay, Optional.fromNullable(attachment.getDigest()));
return new SignalServiceAttachmentPointer(id, null, key, relay, Optional.fromNullable(attachment.getDigest()), Optional.fromNullable(attachment.getFileName()));
} catch (InvalidMessageException | IOException e) {
Log.w(TAG, e);
throw new InvalidPartException(e);

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.InvalidMessageException;
import java.io.IOException;
import java.util.Arrays;
public class AttachmentFileNameJob extends MasterSecretJob {
private static final long serialVersionUID = 1L;
private final long attachmentRowId;
private final long attachmentUniqueId;
private final String encryptedFileName;
public AttachmentFileNameJob(@NonNull Context context, @NonNull AsymmetricMasterSecret asymmetricMasterSecret,
@NonNull DatabaseAttachment attachment, @NonNull IncomingMediaMessage message)
{
super(context, new JobParameters.Builder().withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.attachmentRowId = attachment.getAttachmentId().getRowId();
this.attachmentUniqueId = attachment.getAttachmentId().getUniqueId();
this.encryptedFileName = getEncryptedFileName(asymmetricMasterSecret, attachment, message);
}
@Override
public void onRun(MasterSecret masterSecret) throws IOException, InvalidMessageException {
if (encryptedFileName == null) return;
AttachmentId attachmentId = new AttachmentId(attachmentRowId, attachmentUniqueId);
String plaintextFileName = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret)).decryptBody(encryptedFileName);
DatabaseFactory.getAttachmentDatabase(context).updateAttachmentFileName(masterSecret, attachmentId, plaintextFileName);
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onAdded() {
}
@Override
public void onCanceled() {
}
private @Nullable String getEncryptedFileName(@NonNull AsymmetricMasterSecret asymmetricMasterSecret,
@NonNull DatabaseAttachment attachment,
@NonNull IncomingMediaMessage mediaMessage)
{
for (Attachment messageAttachment : mediaMessage.getAttachments()) {
if (mediaMessage.getAttachments().size() == 1 ||
(messageAttachment.getDigest() != null && Arrays.equals(messageAttachment.getDigest(), attachment.getDigest())))
{
if (messageAttachment.getFileName() == null) return null;
else return new AsymmetricMasterCipher(asymmetricMasterSecret).encryptBody(messageAttachment.getFileName());
}
}
return null;
}
}

View File

@@ -65,6 +65,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
byte[] key = record.getAvatarKey();
String relay = record.getRelay();
Optional<byte[]> digest = Optional.fromNullable(record.getAvatarDigest());
Optional<String> fileName = Optional.absent();
if (avatarId == -1 || key == null) {
return;
@@ -77,7 +78,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
attachment.deleteOnExit();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest);
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest, fileName);
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500);

View File

@@ -185,10 +185,14 @@ public class MmsDownloadJob extends MasterSecretJob {
PduPart part = media.getPart(i);
if (part.getData() != null) {
Uri uri = provider.createUri(part.getData());
Uri uri = provider.createUri(part.getData());
String name = null;
if (part.getName() != null) name = Util.toIsoString(part.getName());
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length));
part.getData().length, name));
}
}
}

View File

@@ -7,6 +7,7 @@ import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.attachments.PointerAttachment;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
@@ -76,6 +77,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -483,13 +485,19 @@ public class PushDecryptJob extends ContextJob {
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
if (insertResult.isPresent()) {
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, insertResult.get().getMessageId());
for (DatabaseAttachment attachment : attachments) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(),
attachment.getAttachmentId()));
if (!masterSecret.getMasterSecret().isPresent()) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentFileNameJob(context, masterSecret.getAsymmetricMasterSecret().get(), attachment, mediaMessage));
}
}
if (smsMessageId.isPresent()) {
@@ -550,7 +558,7 @@ public class PushDecryptJob extends ContextJob {
database.markAsSent(messageId, true);
for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) {
for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, messageId)) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId()));

View File

@@ -74,27 +74,23 @@ public abstract class PushSendJob extends SendJob {
List<SignalServiceAttachment> attachments = new LinkedList<>();
for (final Attachment attachment : parts) {
if (ContentType.isImageType(attachment.getContentType()) ||
ContentType.isAudioType(attachment.getContentType()) ||
ContentType.isVideoType(attachment.getContentType()))
{
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
attachments.add(SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
.withLength(attachment.getSize())
.withListener(new ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
}
})
.build());
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
attachments.add(SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withListener(new ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
}
})
.build());
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
}

View File

@@ -19,6 +19,8 @@ import org.whispersystems.jobqueue.requirements.Requirement;
import java.util.Collections;
import java.util.Set;
import ws.com.google.android.mms.ContentType;
public class MediaNetworkRequirement implements Requirement, ContextDependent {
private static final long serialVersionUID = 0L;
private static final String TAG = MediaNetworkRequirement.class.getSimpleName();
@@ -76,7 +78,7 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
public boolean isPresent() {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context);
final Attachment attachment = db.getAttachment(attachmentId);
final Attachment attachment = db.getAttachment(null, attachmentId);
if (attachment == null) {
Log.w(TAG, "attachment was null, returning vacuous true");
@@ -89,7 +91,15 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
return true;
case AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING:
final Set<String> allowedTypes = getAllowedAutoDownloadTypes();
final boolean isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(attachment.getContentType()));
final String contentType = attachment.getContentType();
boolean isAllowed;
if (isNonDocumentType(contentType)) {
isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
} else {
isAllowed = allowedTypes.contains("documents");
}
/// XXX WTF -- This is *hella* gross. A requirement shouldn't have the side effect of
// *modifying the database* just by calling isPresent().
@@ -99,4 +109,11 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
return false;
}
}
private boolean isNonDocumentType(String contentType) {
return
ContentType.isImageType(contentType) ||
ContentType.isVideoType(contentType) ||
ContentType.isAudioType(contentType);
}
}