From e96bf2bdc7a9cb7497eec696362c29a764d292d7 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 10 May 2017 15:21:52 -0700 Subject: [PATCH] Allow share intents for arbitrary file types Fixes #6608 // FREEBIE --- AndroidManifest.xml | 3 + .../securesms/ConversationActivity.java | 4 +- .../thoughtcrime/securesms/ShareActivity.java | 21 ++++- .../securesms/audio/AudioRecorder.java | 2 +- .../securesms/mms/AttachmentManager.java | 25 +++-- .../securesms/mms/PartAuthority.java | 72 ++++++++++++--- .../providers/PersistentBlobProvider.java | 91 +++++++++++++++---- .../securesms/scribbles/ScribbleActivity.java | 2 +- 8 files changed, 181 insertions(+), 39 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f486b08f11..a1441f818d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -165,6 +165,9 @@ + + + () { + new AsyncTask() { @Override protected void onPreExecute() { thumbnail.clear(); @@ -285,11 +285,23 @@ public class AttachmentManager { } private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri) throws IOException { - long start = System.currentTimeMillis(); - long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri); + long start = System.currentTimeMillis(); + Long mediaSize = null; + String fileName = null; + String mimeType = null; + + if (PartAuthority.isLocalUri(uri)) { + mediaSize = PartAuthority.getAttachmentSize(context, masterSecret, uri); + fileName = PartAuthority.getAttachmentFileName(context, masterSecret, uri); + mimeType = PartAuthority.getAttachmentContentType(context, masterSecret, uri); + } + + if (mediaSize == null) { + mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri); + } Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, null, null, mediaSize); + return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize); } }.execute(); } @@ -466,7 +478,8 @@ public class AttachmentManager { if (MediaUtil.isImageType(mimeType)) return IMAGE; if (MediaUtil.isAudioType(mimeType)) return AUDIO; if (MediaUtil.isVideoType(mimeType)) return VIDEO; - return null; + + return DOCUMENT; } } diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index ffca98b99f..3fa2391ae2 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -5,7 +5,9 @@ import android.content.Context; import android.content.UriMatcher; import android.net.Uri; 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.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -34,7 +36,8 @@ public class PartAuthority { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW); uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW); - uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH, PERSISTENT_ROW); + uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); + uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW); } @@ -44,21 +47,68 @@ public class PartAuthority { int match = uriMatcher.match(uri); try { switch (match) { + case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(masterSecret, new PartUriParser(uri).getPartId()); + case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, new PartUriParser(uri).getPartId()); + case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); + case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); + default: return context.getContentResolver().openInputStream(uri); + } + } catch (SecurityException se) { + throw new IOException(se); + } + } + + public static @Nullable String getAttachmentFileName(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case THUMB_ROW: + case PART_ROW: + Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId()); + + if (attachment != null) return attachment.getFileName(); + else return null; + case PERSISTENT_ROW: + return PersistentBlobProvider.getFileName(context, masterSecret, uri); + case SINGLE_USE_ROW: + default: + return null; + } + } + + public static @Nullable Long getAttachmentSize(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case THUMB_ROW: case PART_ROW: - PartUriParser partUri = new PartUriParser(uri); - return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(masterSecret, partUri.getPartId()); + Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId()); + + if (attachment != null) return attachment.getSize(); + else return null; + case PERSISTENT_ROW: + return PersistentBlobProvider.getFileSize(context, uri); + case SINGLE_USE_ROW: + default: + return null; + } + } + + public static @Nullable String getAttachmentContentType(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { case THUMB_ROW: - partUri = new PartUriParser(uri); - return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId()); + case PART_ROW: + Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId()); + + if (attachment != null) return attachment.getContentType(); + else return null; case PERSISTENT_ROW: - return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); + return PersistentBlobProvider.getMimeType(context, uri); case SINGLE_USE_ROW: - return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); default: - return context.getContentResolver().openInputStream(uri); - } - } catch (SecurityException se) { - throw new IOException(se); + return null; } } diff --git a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java index f4755893ae..931b7d3f68 100644 --- a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java +++ b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java @@ -12,8 +12,10 @@ import android.webkit.MimeTypeMap; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; +import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.InvalidMessageException; import java.io.ByteArrayInputStream; import java.io.File; @@ -30,15 +32,23 @@ public class PersistentBlobProvider { private static final String TAG = PersistentBlobProvider.class.getSimpleName(); - private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; + private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new"; public static final Uri CONTENT_URI = Uri.parse(URI_STRING); public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String EXPECTED_PATH = "capture/*/*/#"; + public static final String EXPECTED_PATH_OLD = "capture/*/*/#"; + public static final String EXPECTED_PATH_NEW = "capture-new/*/*/*/*/#"; + private static final int MIMETYPE_PATH_SEGMENT = 1; + private static final int FILENAME_PATH_SEGMENT = 2; + private static final int FILESIZE_PATH_SEGMENT = 3; + private static final String BLOB_EXTENSION = "blob"; - private static final int MATCH = 1; + private static final int MATCH_OLD = 1; + private static final int MATCH_NEW = 2; + private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{ - addURI(AUTHORITY, EXPECTED_PATH, MATCH); + addURI(AUTHORITY, EXPECTED_PATH_OLD, MATCH_OLD); + addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW); }}; private static volatile PersistentBlobProvider instance; @@ -63,26 +73,37 @@ public class PersistentBlobProvider { this.context = context.getApplicationContext(); } - public Uri create(@NonNull MasterSecret masterSecret, - @NonNull byte[] blobBytes, - @NonNull String mimeType) + public Uri create(@NonNull MasterSecret masterSecret, + @NonNull byte[] blobBytes, + @NonNull String mimeType, + @Nullable String fileName) { final long id = System.currentTimeMillis(); cache.put(id, blobBytes); - return create(masterSecret, new ByteArrayInputStream(blobBytes), id, mimeType); + return create(masterSecret, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length); } - public Uri create(@NonNull MasterSecret masterSecret, - @NonNull InputStream input, - @NonNull String mimeType) + public Uri create(@NonNull MasterSecret masterSecret, + @NonNull InputStream input, + @NonNull String mimeType, + @Nullable String fileName, + @Nullable Long fileSize) { - return create(masterSecret, input, System.currentTimeMillis(), mimeType); + return create(masterSecret, input, System.currentTimeMillis(), mimeType, fileName, fileSize); } - private Uri create(MasterSecret masterSecret, InputStream input, long id, String mimeType) { + private Uri create(@NonNull MasterSecret masterSecret, + @NonNull InputStream input, + long id, + @NonNull String mimeType, + @Nullable String fileName, + @Nullable Long fileSize) + { persistToDisk(masterSecret, id, input); final Uri uniqueUri = CONTENT_URI.buildUpon() .appendPath(mimeType) + .appendPath(getEncryptedFileName(masterSecret, fileName)) + .appendEncodedPath(String.valueOf(fileSize)) .appendEncodedPath(String.valueOf(System.currentTimeMillis())) .build(); return ContentUris.withAppendedId(uniqueUri, id); @@ -113,13 +134,14 @@ public class PersistentBlobProvider { public boolean delete(@NonNull Uri uri) { switch (MATCHER.match(uri)) { - case MATCH: + case MATCH_OLD: + case MATCH_NEW: long id = ContentUris.parseId(uri); cache.remove(id); return getFile(ContentUris.parseId(uri)).delete(); - default: - return new File(uri.getPath()).delete(); } + + return false; } public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException { @@ -132,6 +154,11 @@ public class PersistentBlobProvider { return new File(context.getDir("captures", Context.MODE_PRIVATE), id + "." + BLOB_EXTENSION); } + private @Nullable String getEncryptedFileName(@NonNull MasterSecret masterSecret, @Nullable String fileName) { + if (fileName == null) return null; + return new MasterCipher(masterSecret).encryptBody(fileName); + } + public static @Nullable String getMimeType(@NonNull Context context, @NonNull Uri persistentBlobUri) { if (!isAuthority(context, persistentBlobUri)) return null; return isExternalBlobUri(context, persistentBlobUri) @@ -139,6 +166,35 @@ public class PersistentBlobProvider { : persistentBlobUri.getPathSegments().get(MIMETYPE_PATH_SEGMENT); } + public static @Nullable String getFileName(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri persistentBlobUri) { + if (!isAuthority(context, persistentBlobUri)) return null; + if (isExternalBlobUri(context, persistentBlobUri)) return null; + if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null; + + String fileName = persistentBlobUri.getPathSegments().get(FILENAME_PATH_SEGMENT); + + try { + return new MasterCipher(masterSecret).decryptBody(fileName); + } catch (InvalidMessageException e) { + Log.w(TAG, "No valid filename for URI"); + } + + return null; + } + + public static @Nullable Long getFileSize(@NonNull Context context, Uri persistentBlobUri) { + if (!isAuthority(context, persistentBlobUri)) return null; + if (isExternalBlobUri(context, persistentBlobUri)) return null; + if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null; + + try { + return Long.valueOf(persistentBlobUri.getPathSegments().get(FILESIZE_PATH_SEGMENT)); + } catch (NumberFormatException e) { + Log.w(TAG, e); + return null; + } + } + private static @NonNull String getExtensionFromMimeType(String mimeType) { final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); return extension != null ? extension : BLOB_EXTENSION; @@ -157,7 +213,8 @@ public class PersistentBlobProvider { } public static boolean isAuthority(@NonNull Context context, @NonNull Uri uri) { - return MATCHER.match(uri) == MATCH || isExternalBlobUri(context, uri); + int matchResult = MATCHER.match(uri); + return matchResult == MATCH_NEW || matchResult == MATCH_OLD || isExternalBlobUri(context, uri); } private static boolean isExternalBlobUri(@NonNull Context context, @NonNull Uri uri) { diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java index ff986c56aa..bb06fda0c1 100644 --- a/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java +++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java @@ -229,7 +229,7 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity implem baos = null; result = null; - Uri uri = provider.create(masterSecret, data, MediaUtil.IMAGE_JPEG); + Uri uri = provider.create(masterSecret, data, MediaUtil.IMAGE_JPEG, null); Intent intent = new Intent(); intent.setData(uri); setResult(RESULT_OK, intent);