From 7384fa7f2b4ff158e65cda787a58b64dc306691c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 15 Oct 2015 19:37:08 +0200 Subject: Rename TemporaryStorageProvider to TemporaryFileProvider, use interface for db contract --- .../keychain/ui/SymmetricTextOperationTests.java | 4 +- .../keychain/ui/ViewKeyAdvShareTest.java | 6 +- OpenKeychain/src/main/AndroidManifest.xml | 2 +- .../keychain/KeychainApplication.java | 4 +- .../keychain/operations/ExportOperation.java | 4 +- .../keychain/operations/InputDataOperation.java | 10 +- .../keychain/provider/TemporaryFileProvider.java | 296 +++++++++++++++++++++ .../provider/TemporaryStorageProvider.java | 288 -------------------- .../keychain/ui/BackupCodeFragment.java | 4 +- .../keychain/ui/DecryptActivity.java | 4 +- .../keychain/ui/EncryptFilesFragment.java | 6 +- .../keychain/ui/LogDisplayFragment.java | 4 +- .../keychain/ui/ViewKeyAdvShareFragment.java | 6 +- .../keychain/operations/ExportTest.java | 4 +- .../keychain/pgp/InputDataOperationTest.java | 8 +- 15 files changed, 329 insertions(+), 321 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java index 498df7299..ba5eb7491 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java @@ -32,7 +32,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import static android.support.test.InstrumentationRegistry.getInstrumentation; @@ -133,7 +133,7 @@ public class SymmetricTextOperationTests { hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( hasAction(Intent.ACTION_VIEW), hasFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), - hasData(allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY))), + hasData(allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY))), hasType("text/plain") )) )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java index 63c7dc6de..edc5571fe 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java @@ -34,7 +34,7 @@ import org.junit.runners.MethodSorters; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import static android.support.test.espresso.Espresso.onView; @@ -96,7 +96,7 @@ public class ViewKeyAdvShareTest { hasType("text/plain"), hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")), hasExtra(is(Intent.EXTRA_STREAM), - allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY))) + allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY))) )) )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); onView(withId(R.id.view_key_action_fingerprint_share)).perform(click()); @@ -113,7 +113,7 @@ public class ViewKeyAdvShareTest { hasType("text/plain"), hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")), hasExtra(is(Intent.EXTRA_STREAM), - allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY))) + allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY))) )) )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); onView(withId(R.id.view_key_action_key_share)).perform(click()); diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 2f31d6d63..3cac4ed10 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -845,7 +845,7 @@ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index ebd48b9a5..5d97dac8a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -33,7 +33,7 @@ import android.widget.Toast; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; @@ -102,7 +102,7 @@ public class KeychainApplication extends Application { TlsHelper.addPinnedCertificate("pgp.mit.edu", getAssets(), "pgp.mit.edu.cer"); TlsHelper.addPinnedCertificate("api.keybase.io", getAssets(), "api.keybase.io.CA.cer"); - TemporaryStorageProvider.cleanUp(this); + TemporaryFileProvider.cleanUp(this); if (!checkConsolidateRecovery()) { // force DB upgrade, https://github.com/open-keychain/open-keychain/issues/1334 diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java index ecff9f5ae..5f77a3224 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java @@ -54,7 +54,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -111,7 +111,7 @@ public class ExportOperation extends BaseOperation { Uri exportOutputUri = nonEncryptedOutput ? exportInput.mOutputUri - : TemporaryStorageProvider.createFile(mContext); + : TemporaryFileProvider.createFile(mContext); int exportedDataSize; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java index c48ccc500..2b91cd06c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java @@ -48,7 +48,7 @@ import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.InputDataParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -101,7 +101,7 @@ public class InputDataOperation extends BaseOperation { decryptInput.setInputUri(input.getInputUri()); - currentInputUri = TemporaryStorageProvider.createFile(mContext); + currentInputUri = TemporaryFileProvider.createFile(mContext); decryptInput.setOutputUri(currentInputUri); decryptResult = op.execute(decryptInput, cryptoInput); @@ -117,7 +117,7 @@ public class InputDataOperation extends BaseOperation { // inform the storage provider about the mime type for this uri if (decryptResult.getDecryptionMetadata() != null) { - TemporaryStorageProvider.setMimeType(mContext, currentInputUri, + TemporaryFileProvider.setMimeType(mContext, currentInputUri, decryptResult.getDecryptionMetadata().getMimeType()); } @@ -195,7 +195,7 @@ public class InputDataOperation extends BaseOperation { log.add(LogType.MSG_DATA_DETACHED_RAW, 3); - uncheckedSignedDataUri = TemporaryStorageProvider.createFile(mContext, mFilename, "text/plain"); + uncheckedSignedDataUri = TemporaryFileProvider.createFile(mContext, mFilename, "text/plain"); OutputStream out = mContext.getContentResolver().openOutputStream(uncheckedSignedDataUri, "w"); if (out == null) { @@ -297,7 +297,7 @@ public class InputDataOperation extends BaseOperation { log.add(LogType.MSG_DATA_MIME_FILENAME, 3, mFilename); } - Uri uri = TemporaryStorageProvider.createFile(mContext, mFilename, bd.getMimeType()); + Uri uri = TemporaryFileProvider.createFile(mContext, mFilename, bd.getMimeType()); OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w"); if (out == null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java new file mode 100644 index 000000000..2995ae88a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2014 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + + +import android.content.ClipDescription; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.DatabaseUtil; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.File; +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 TemporaryFileProvider extends ContentProvider { + + private static final String DB_NAME = "tempstorage.db"; + private static final String TABLE_FILES = "files"; + public static final String AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); + private static final int DB_VERSION = 3; + + interface TemporaryFileColumns { + String COLUMN_UUID = "id"; + String COLUMN_NAME = "name"; + String COLUMN_TIME = "time"; + String COLUMN_TYPE = "mimetype"; + } + + private static File cacheDir; + + public static Uri createFile(Context context, String targetName, String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); + contentValues.put(TemporaryFileColumns.COLUMN_TYPE, mimeType); + return context.getContentResolver().insert(CONTENT_URI, contentValues); + } + + public static Uri createFile(Context context, String targetName) { + ContentValues contentValues = new ContentValues(); + contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); + return context.getContentResolver().insert(CONTENT_URI, contentValues); + } + + public static Uri createFile(Context context) { + ContentValues contentValues = new ContentValues(); + return context.getContentResolver().insert(CONTENT_URI, contentValues); + } + + public static int setMimeType(Context context, Uri uri, String mimetype) { + ContentValues values = new ContentValues(); + values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype); + return context.getContentResolver().update(uri, values, null, null); + } + + public static int cleanUp(Context context) { + return context.getContentResolver().delete( + CONTENT_URI, + TemporaryFileColumns.COLUMN_TIME + "< ?", + new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)} + ); + } + + private class TemporaryStorageDatabase extends SQLiteOpenHelper { + + public TemporaryStorageDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + + TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " + + TemporaryFileColumns.COLUMN_NAME + " TEXT, " + + TemporaryFileColumns.COLUMN_TYPE + " TEXT, " + + TemporaryFileColumns.COLUMN_TIME + " INTEGER" + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(Constants.TAG, "Upgrading files db from " + oldVersion + " to " + newVersion); + + switch (oldVersion) { + case 1: + db.execSQL("DROP TABLE IF EXISTS files"); + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + + TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " + + TemporaryFileColumns.COLUMN_NAME + " TEXT, " + + TemporaryFileColumns.COLUMN_TIME + " INTEGER" + + ");"); + case 2: + db.execSQL("ALTER TABLE files ADD COLUMN " + TemporaryFileColumns.COLUMN_TYPE + " TEXT"); + } + } + } + + private static TemporaryStorageDatabase db; + + private File getFile(Uri uri) throws FileNotFoundException { + try { + return getFile(uri.getLastPathSegment()); + } catch (NumberFormatException e) { + throw new FileNotFoundException(); + } + } + + private File getFile(String id) { + return new File(cacheDir, "temp/" + id); + } + + @Override + public boolean onCreate() { + db = new TemporaryStorageDatabase(getContext()); + cacheDir = getContext().getCacheDir(); + return new File(cacheDir, "temp").mkdirs(); + } + + @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."); + } + + Log.d(Constants.TAG, "being asked for file " + uri); + + File file; + try { + file = getFile(uri); + if (file.exists()) { + Log.e(Constants.TAG, "already exists"); + } + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "file not found!"); + return null; + } + + Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, + new String[]{TemporaryFileColumns.COLUMN_NAME}, + TemporaryFileColumns.COLUMN_UUID + "=?", + 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()); + fileName.close(); + return cursor; + } + fileName.close(); + } + return null; + } + + @Override + public String getType(Uri uri) { + Cursor cursor = db.getReadableDatabase().query(TABLE_FILES, + new String[]{TemporaryFileColumns.COLUMN_TYPE}, TemporaryFileColumns.COLUMN_UUID + "=?", + 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 + public Uri insert(Uri uri, ContentValues values) { + if (!values.containsKey(TemporaryFileColumns.COLUMN_TIME)) { + values.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis()); + } + String uuid = UUID.randomUUID().toString(); + values.put(TemporaryFileColumns.COLUMN_UUID, 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(CONTENT_URI, uuid); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + if (uri == null || uri.getLastPathSegment() == null) { + return 0; + } + + selection = DatabaseUtil.concatenateWhere(selection, TemporaryFileColumns.COLUMN_UUID + "=?"); + selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + + Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{TemporaryFileColumns.COLUMN_UUID}, selection, + selectionArgs, null, null, null); + if (files != null) { + while (files.moveToNext()) { + getFile(files.getString(0)).delete(); + } + files.close(); + return db.getWritableDatabase().delete(TABLE_FILES, selection, selectionArgs); + } + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + if (values.size() != 1 || !values.containsKey(TemporaryFileColumns.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, + TemporaryFileColumns.COLUMN_UUID + " = ?", new String[]{uri.getLastPathSegment()}); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + Log.d(Constants.TAG, "openFile"); + return openFileHelper(uri, mode); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java deleted file mode 100644 index 67f2c36bc..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann - * Copyright (C) 2014 Vincent Breitmoser - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import android.content.ClipDescription; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.provider.OpenableColumns; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.util.DatabaseUtil; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.File; -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"; - private static final String TABLE_FILES = "files"; - private static final String COLUMN_ID = "id"; - private static final String COLUMN_NAME = "name"; - private static final String COLUMN_TIME = "time"; - private static final String COLUMN_TYPE = "mimetype"; - public static final String AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; - public static final Uri CONTENT_URI = Uri.parse("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(CONTENT_URI, contentValues); - } - - public static Uri createFile(Context context, String targetName) { - ContentValues contentValues = new ContentValues(); - contentValues.put(COLUMN_NAME, targetName); - return context.getContentResolver().insert(CONTENT_URI, contentValues); - } - - public static Uri createFile(Context context) { - ContentValues contentValues = new ContentValues(); - return context.getContentResolver().insert(CONTENT_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(CONTENT_URI, COLUMN_TIME + "< ?", - new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); - } - - private class TemporaryStorageDatabase extends SQLiteOpenHelper { - - public TemporaryStorageDatabase(Context context) { - super(context, DB_NAME, null, DB_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + - COLUMN_ID + " TEXT PRIMARY KEY, " + - COLUMN_NAME + " TEXT, " + - COLUMN_TYPE + " TEXT, " + - COLUMN_TIME + " INTEGER" + - ");"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.d(Constants.TAG, "Upgrading files db from " + oldVersion + " to " + newVersion); - - switch (oldVersion) { - case 1: - db.execSQL("DROP TABLE IF EXISTS files"); - db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + - COLUMN_ID + " TEXT PRIMARY KEY, " + - COLUMN_NAME + " TEXT, " + - COLUMN_TIME + " INTEGER" + - ");"); - case 2: - db.execSQL("ALTER TABLE files ADD COLUMN " + COLUMN_TYPE + " TEXT"); - } - } - } - - private static TemporaryStorageDatabase db; - - private File getFile(Uri uri) throws FileNotFoundException { - try { - return getFile(uri.getLastPathSegment()); - } catch (NumberFormatException e) { - throw new FileNotFoundException(); - } - } - - private File getFile(String id) { - return new File(cacheDir, "temp/" + id); - } - - @Override - public boolean onCreate() { - db = new TemporaryStorageDatabase(getContext()); - cacheDir = getContext().getCacheDir(); - return new File(cacheDir, "temp").mkdirs(); - } - - @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."); - } - - Log.d(Constants.TAG, "being asked for file " + uri); - - File file; - try { - file = getFile(uri); - if (file.exists()) { - Log.e(Constants.TAG, "already exists"); - } - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "file not found!"); - return null; - } - - Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_NAME}, COLUMN_ID + "=?", - 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()); - fileName.close(); - return cursor; - } - fileName.close(); - } - return null; - } - - @Override - public String getType(Uri uri) { - 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 - public Uri insert(Uri uri, ContentValues values) { - if (!values.containsKey(COLUMN_TIME)) { - values.put(COLUMN_TIME, System.currentTimeMillis()); - } - 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(CONTENT_URI, uuid); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - 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) { - while (files.moveToNext()) { - getFile(files.getString(0)).delete(); - } - files.close(); - return db.getWritableDatabase().delete(TABLE_FILES, selection, selectionArgs); - } - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - 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 { - Log.d(Constants.TAG, "openFile"); - return openFileHelper(uri, mode); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java index ea548e95b..50ad95540 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java @@ -51,7 +51,7 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -427,7 +427,7 @@ public class BackupCodeFragment extends CryptoOperationFragment