diff options
author | Makoto Onuki <omakoto@google.com> | 2012-08-13 17:56:25 -0700 |
---|---|---|
committer | Makoto Onuki <omakoto@google.com> | 2012-08-14 11:48:17 -0700 |
commit | 623659ebf4875e63bf4fef1e0b00096e09121853 (patch) | |
tree | 87a983ac5b4e9602ee488958294b055d919c087e | |
parent | e8956364a9cf4ec0f463768a6116fdcda8635b13 (diff) | |
download | packages_providers_ContactsProvider-623659ebf4875e63bf4fef1e0b00096e09121853.zip packages_providers_ContactsProvider-623659ebf4875e63bf4fef1e0b00096e09121853.tar.gz packages_providers_ContactsProvider-623659ebf4875e63bf4fef1e0b00096e09121853.tar.bz2 |
Make "export contacts database" more secure
Don't put the dump file on the SD card. Instead, put it in the internal cache
directory which is protected by the filesystem permissions.
In order to make it attachable on gmail, create a shim content provider and
sends a content: URI for this provider.
The dump file can be read only from the apps that knows its name, which
we pass via the SEND intent. Each dump file has a unique 256-bit random name,
so it's virtually impossible to for other apps to read them.
Bug 6813842
Change-Id: I3ca081e696e4e432e2bf7eb701595c508cd19409
-rw-r--r-- | AndroidManifest.xml | 5 | ||||
-rw-r--r-- | res/values/strings.xml | 4 | ||||
-rw-r--r-- | src/com/android/providers/contacts/debug/ContactsDumpActivity.java | 36 | ||||
-rw-r--r-- | src/com/android/providers/contacts/debug/DataExporter.java | 104 | ||||
-rw-r--r-- | src/com/android/providers/contacts/debug/DumpFileProvider.java | 113 |
5 files changed, 222 insertions, 40 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4370a42..7385fd8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -110,5 +110,10 @@ </intent-filter> </activity> + <provider android:name=".debug.DumpFileProvider" + android:authorities="com.android.contacts.dumpfile" + android:exported="true"> + </provider> + </application> </manifest> diff --git a/res/values/strings.xml b/res/values/strings.xml index e17b5ed..5291017 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -62,8 +62,8 @@ <!-- Debug tool - title of the dialog which copies the contact database into the external storage. [CHAR LIMIT=NONE] --> <string name="debug_dump_title">Copy contacts database</string> - <!-- Debug tool - message shown to the user on the dialog which copies the contact database into the external storage. [CHAR LIMIT=NONE] --> - <string name="debug_dump_database_message">You are about to 1) make a copy of your database which includes all contacts related information and all call log to the SD card/USB storage, which is readable by any app, and 2) email it. Remember to delete the copy as soon as you have successfully copied it off the device or the email is received.</string> + <!-- Debug tool - message shown to the user on the dialog which sends a copy of the contact database via email or other apps. [CHAR LIMIT=NONE] --> + <string name="debug_dump_database_message">You are about to 1) make a copy of your database which includes all contacts related information and all call log to the internal storage, and 2) email it. Remember to delete the copy as soon as you have successfully copied it off the device or the email is received.</string> <!-- Debug tool - dialog button- delete file now [CHAR LIMIT=NONE] --> <string name="debug_dump_delete_button">Delete now</string> diff --git a/src/com/android/providers/contacts/debug/ContactsDumpActivity.java b/src/com/android/providers/contacts/debug/ContactsDumpActivity.java index 530e779..359f3f8 100644 --- a/src/com/android/providers/contacts/debug/ContactsDumpActivity.java +++ b/src/com/android/providers/contacts/debug/ContactsDumpActivity.java @@ -16,21 +16,19 @@ package com.android.providers.contacts.debug; +import com.android.providers.contacts.R; + import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Environment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.widget.Button; -import com.android.providers.contacts.R; - -import java.io.File; import java.io.IOException; /** @@ -45,9 +43,6 @@ public class ContactsDumpActivity extends Activity implements OnClickListener { private Button mCancelButton; private Button mDeleteButton; - private static final File OUT_FILE = new File(Environment.getExternalStorageDirectory(), - "contacts.db.zip"); - @Override protected void onCreate(Bundle savedInstanceState) { // Be sure to call the super class. @@ -67,7 +62,7 @@ public class ContactsDumpActivity extends Activity implements OnClickListener { } private void updateDeleteButton() { - mDeleteButton.setEnabled(OUT_FILE.exists()); + mDeleteButton.setEnabled(DataExporter.dumpFileExists(this)); } @Override @@ -89,11 +84,10 @@ public class ContactsDumpActivity extends Activity implements OnClickListener { } private void cleanup() { - Log.i(TAG, "Deleting " + OUT_FILE); - OUT_FILE.delete(); + DataExporter.removeDumpFiles(this); } - private class DumpDbTask extends AsyncTask<Void, Void, Boolean> { + private class DumpDbTask extends AsyncTask<Void, Void, Uri> { /** * Starts spinner while task is running. */ @@ -103,32 +97,30 @@ public class ContactsDumpActivity extends Activity implements OnClickListener { } @Override - protected Boolean doInBackground(Void... params) { + protected Uri doInBackground(Void... params) { try { - DataExporter.exportData(getApplicationContext(), OUT_FILE); - return true; + return DataExporter.exportData(getApplicationContext()); } catch (IOException e) { Log.e(TAG, "Failed to export", e); - return false; + return null; } } @Override - protected void onPostExecute(Boolean success) { - if (success != null && success) { - emailFile(OUT_FILE); + protected void onPostExecute(Uri uri) { + if (uri != null) { + emailFile(uri); } } } - private void emailFile(File file) { - Log.i(TAG, "Drafting email to send " + file.getAbsolutePath() + - " (" + file.length() + " bytes)"); + private void emailFile(Uri uri) { + Log.i(TAG, "Drafting email"); Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.debug_dump_email_subject)); intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.debug_dump_email_body)); intent.setType(DataExporter.ZIP_MIME_TYPE); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); + intent.putExtra(Intent.EXTRA_STREAM, uri); startActivityForResult(Intent.createChooser(intent, getString(R.string.debug_dump_email_sender_picker)), 0); } diff --git a/src/com/android/providers/contacts/debug/DataExporter.java b/src/com/android/providers/contacts/debug/DataExporter.java index 886314b..6771c1a 100644 --- a/src/com/android/providers/contacts/debug/DataExporter.java +++ b/src/com/android/providers/contacts/debug/DataExporter.java @@ -16,58 +16,129 @@ package com.android.providers.contacts.debug; +import com.android.providers.contacts.util.Hex; +import com.google.common.io.Closeables; + import android.content.Context; -import android.media.MediaScannerConnection; +import android.net.Uri; import android.util.Log; -import com.google.common.io.Closeables; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.security.SecureRandom; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Compress all files under the app data dir into a single zip file. + * + * Make sure not to output dump filenames anywhere, including logcat. */ public class DataExporter { private static String TAG = "DataExporter"; public static final String ZIP_MIME_TYPE = "application/zip"; + public static final String DUMP_FILE_DIRECTORY_NAME = "dumpedfiles"; + + public static final String OUT_FILE_SUFFIX = "-contacts-db.zip"; + /** - * Compress all files under the app data dir into a single zip file. + * Compress all files under the app data dir into a single zip file, and return the content:// + * URI to the file, which can be read via {@link DumpFileProvider}. */ - public static void exportData(Context context, File outFile) throws IOException { - outFile.delete(); - Log.i(TAG, "Outfile=" + outFile.getAbsolutePath()); + public static Uri exportData(Context context) throws IOException { + final String fileName = generateRandomName() + OUT_FILE_SUFFIX; + final File outFile = getOutputFile(context, fileName); + + // Remove all existing ones. + removeDumpFiles(context); + Log.i(TAG, "Dump started..."); + + ensureOutputDirectory(context); final ZipOutputStream os = new ZipOutputStream(new FileOutputStream(outFile)); try { - addDirectory(os, context.getFilesDir().getParentFile(), "contacts-files"); + addDirectory(context, os, context.getFilesDir().getParentFile(), "contacts-files"); } finally { Closeables.closeQuietly(os); } - // Tell the media scanner about the new file so that it is - // immediately available to the user. - MediaScannerConnection.scanFile(context, - new String[] {outFile.toString()}, - new String[] {ZIP_MIME_TYPE}, null); + Log.i(TAG, "Dump finished."); + return DumpFileProvider.AUTHORITY_URI.buildUpon().appendPath(fileName).build(); + } + + /** @return long random string for a file name */ + private static String generateRandomName() { + final SecureRandom rng = new SecureRandom(); + final byte[] random = new byte[256 / 8]; + rng.nextBytes(random); + + return Hex.encodeHex(random, true); + } + + private static File getOutputDirectory(Context context) { + return new File(context.getCacheDir(), DUMP_FILE_DIRECTORY_NAME); + } + + private static void ensureOutputDirectory(Context context) { + final File directory = getOutputDirectory(context); + if (!directory.exists()) { + directory.mkdir(); + } + } + + public static File getOutputFile(Context context, String fileName) { + return new File(getOutputDirectory(context), fileName); + } + + public static boolean dumpFileExists(Context context) { + return getOutputDirectory(context).exists(); + } + + public static void removeDumpFiles(Context context) { + removeFileOrDirectory(getOutputDirectory(context)); + } + + private static void removeFileOrDirectory(File file) { + if (!file.exists()) return; + + if (file.isFile()) { + Log.i(TAG, "Removing " + file); + file.delete(); + return; + } + + if (file.isDirectory()) { + for (File child : file.listFiles()) { + removeFileOrDirectory(child); + } + Log.i(TAG, "Removing " + file); + file.delete(); + } } /** * Add all files under {@code current} to {@code os} zip stream */ - private static void addDirectory(ZipOutputStream os, File current, String storedPath) - throws IOException { + private static void addDirectory(Context context, ZipOutputStream os, File current, + String storedPath) throws IOException { for (File child : current.listFiles()) { final String childStoredPath = storedPath + "/" + child.getName(); if (child.isDirectory()) { - addDirectory(os, child, childStoredPath); + // Don't need the cache directory, which also contains the dump files. + if (child.equals(context.getCacheDir())) { + continue; + } + // This check is redundant as the output directory should be in the cache dir, + // but just in case... + if (child.getName().equals(DUMP_FILE_DIRECTORY_NAME)) { + continue; + } + addDirectory(context, os, child, childStoredPath); } else if (child.isFile()) { addFile(os, child, childStoredPath); } else { @@ -82,6 +153,7 @@ public class DataExporter { */ private static void addFile(ZipOutputStream os, File current, String storedPath) throws IOException { + Log.i(TAG, "Adding " + current.getAbsolutePath() + " ..."); final InputStream is = new FileInputStream(current); os.putNextEntry(new ZipEntry(storedPath)); diff --git a/src/com/android/providers/contacts/debug/DumpFileProvider.java b/src/com/android/providers/contacts/debug/DumpFileProvider.java new file mode 100644 index 0000000..1fbe43c --- /dev/null +++ b/src/com/android/providers/contacts/debug/DumpFileProvider.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.contacts.debug; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; + +import java.io.File; +import java.io.FileNotFoundException; + +/** + * Provider used to read dump files created by {@link DataExporter}. + * + * We send content: URI to sender apps (such as gmail). This provider implement the URI. + */ +public class DumpFileProvider extends ContentProvider { + public static final String AUTHORITY = "com.android.contacts.dumpfile"; + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + // Not needed. + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // Not needed. + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + // Not needed. + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + return DataExporter.ZIP_MIME_TYPE; + } + + /** @return the path part of a URI, without the beginning "/". */ + private static String extractFileName(Uri uri) { + final String path = uri.getPath(); + return path.startsWith("/") ? path.substring(1) : path; + } + + /** @return file content */ + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + if (!"r".equals(mode)) { + throw new UnsupportedOperationException(); + } + final File file = DataExporter.getOutputFile(getContext(), extractFileName(uri)); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } + + /** + * Used to provide {@link OpenableColumns#DISPLAY_NAME} and {@link OpenableColumns#SIZE} + * for a URI. + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + if (projection == null) throw new IllegalArgumentException("Projection must not be null"); + + final MatrixCursor c = new MatrixCursor(projection); + + // Result will always have one row. + final MatrixCursor.RowBuilder b = c.newRow(); + + for (int i = 0; i < c.getColumnCount(); i++) { + final String column = projection[i]; + if (OpenableColumns.DISPLAY_NAME.equals(column)) { + // Just return the requested path as the display name. We don't care if the file + // really exists. + b.add(extractFileName(uri)); + } else if (OpenableColumns.SIZE.equals(column)) { + // Always return "unkown" for file size. + b.add(null); + } else { + throw new IllegalArgumentException("Unknown column " + column); + } + } + + return c; + } +} |