diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java')
-rw-r--r-- | OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java | 115 |
1 files changed, 103 insertions, 12 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java index 45f806960..7e9b24989 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java @@ -18,6 +18,8 @@ package org.sufficientlysecure.keychain.provider; + +import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -38,6 +40,25 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.UUID; +/** + * TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to + * sharing them with other applications. + * + * Security: + * - It is writable by OpenKeychain only (see Manifest), but exported for reading files + * - It uses UUIDs as identifiers which makes predicting files from outside impossible + * - Querying a number of files is not allowed, only querying single files + * -> You can only open a file if you know the Uri containing the precise UUID, this Uri is only + * revealed when the user shares a decrypted file with another app. + * + * Why is support lib's FileProvider not used? + * Because granting Uri permissions temporarily does not work correctly. See + * - https://code.google.com/p/android/issues/detail?id=76683 + * - https://github.com/nmr8acme/FileProvider-permission-bug + * - http://stackoverflow.com/q/24467696 + * - http://stackoverflow.com/q/18249007 + * - Comments at http://www.blogc.at/2014/03/23/share-private-files-with-other-apps-fileprovider/ + */ public class TemporaryStorageProvider extends ContentProvider { private static final String DB_NAME = "tempstorage.db"; @@ -45,17 +66,37 @@ public class TemporaryStorageProvider extends ContentProvider { private static final String COLUMN_ID = "id"; private static final String COLUMN_NAME = "name"; private static final String COLUMN_TIME = "time"; - private static final Uri BASE_URI = Uri.parse("content://org.sufficientlysecure.keychain.tempstorage/"); - private static final int DB_VERSION = 2; + private static final String COLUMN_TYPE = "mimetype"; + public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; + private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY); + private static final int DB_VERSION = 3; private static File cacheDir; + public static Uri createFile(Context context, String targetName, String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(COLUMN_NAME, targetName); + contentValues.put(COLUMN_TYPE, mimeType); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + public static Uri createFile(Context context, String targetName) { ContentValues contentValues = new ContentValues(); contentValues.put(COLUMN_NAME, targetName); return context.getContentResolver().insert(BASE_URI, contentValues); } + public static Uri createFile(Context context) { + ContentValues contentValues = new ContentValues(); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + + public static int setMimeType(Context context, Uri uri, String mimetype) { + ContentValues values = new ContentValues(); + values.put(COLUMN_TYPE, mimetype); + return context.getContentResolver().update(uri, values, null, null); + } + public static int cleanUp(Context context) { return context.getContentResolver().delete(BASE_URI, COLUMN_TIME + "< ?", new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); @@ -72,6 +113,7 @@ public class TemporaryStorageProvider extends ContentProvider { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + COLUMN_ID + " TEXT PRIMARY KEY, " + COLUMN_NAME + " TEXT, " + + COLUMN_TYPE + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); } @@ -88,6 +130,8 @@ public class TemporaryStorageProvider extends ContentProvider { COLUMN_NAME + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); + case 2: + db.execSQL("ALTER TABLE files ADD COLUMN " + COLUMN_TYPE + " TEXT"); } } } @@ -115,6 +159,10 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (uri.getLastPathSegment() == null) { + throw new SecurityException("Listing temporary files is not allowed, only querying single files."); + } + File file; try { file = getFile(uri); @@ -125,9 +173,15 @@ public class TemporaryStorageProvider extends ContentProvider { new String[]{uri.getLastPathSegment()}, null, null, null); if (fileName != null) { if (fileName.moveToNext()) { - MatrixCursor cursor = - new MatrixCursor(new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, "_data"}); - cursor.newRow().add(fileName.getString(0)).add(file.length()).add(file.getAbsolutePath()); + MatrixCursor cursor = new MatrixCursor(new String[]{ + OpenableColumns.DISPLAY_NAME, + OpenableColumns.SIZE, + "_data" + }); + cursor.newRow() + .add(fileName.getString(0)) + .add(file.length()) + .add(file.getAbsolutePath()); fileName.close(); return cursor; } @@ -138,9 +192,30 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public String getType(Uri uri) { - // Note: If we can find a files mime type, we can decrypt it to temp storage and open it after - // encryption. The mime type is needed, else UI really sucks and some apps break. - return "*/*"; + Cursor cursor = db.getReadableDatabase().query(TABLE_FILES, + new String[]{COLUMN_TYPE}, COLUMN_ID + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + if (!cursor.isNull(0)) { + return cursor.getString(0); + } + } + } finally { + cursor.close(); + } + } + return "application/octet-stream"; + } + + @Override + public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + String type = getType(uri); + if (ClipDescription.compareMimeTypes(type, mimeTypeFilter)) { + return new String[]{type}; + } + return null; } @Override @@ -151,9 +226,14 @@ public class TemporaryStorageProvider extends ContentProvider { String uuid = UUID.randomUUID().toString(); values.put(COLUMN_ID, uuid); int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values); + if (insert == -1) { + Log.e(Constants.TAG, "Insert failed!"); + return null; + } try { getFile(uuid).createNewFile(); } catch (IOException e) { + Log.e(Constants.TAG, "File creation failed!"); return null; } return Uri.withAppendedPath(BASE_URI, uuid); @@ -161,10 +241,13 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - if (uri.getLastPathSegment() != null) { - selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); - selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + if (uri == null || uri.getLastPathSegment() == null) { + return 0; } + + selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); + selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_ID}, selection, selectionArgs, null, null, null); if (files != null) { @@ -179,11 +262,19 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("Update not supported"); + if (values.size() != 1 || !values.containsKey(COLUMN_TYPE)) { + throw new UnsupportedOperationException("Update supported only for type field!"); + } + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException("Update supported only for plain uri!"); + } + return db.getWritableDatabase().update(TABLE_FILES, values, + COLUMN_ID + " = ?", new String[]{uri.getLastPathSegment()}); } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { return openFileHelper(uri, mode); } + } |