Use FileProvider when bluetoothing apks on API >= 24.

Reuses the code that the installer uses, when it broadcasts to
the relevant installer that an Apk is available for install.

This used to do the following:
 * Copy file to a private directory
 * Make the file world readable (so that PM can access it)
 * Send a file:// URI to the installer

The file:// URI is no longer supported for reasons explained in
the support lib FileProvider class. Now a content:// URI is required,
and that must explicitly grant permission to certain packages.

The existing code here used to grant permission to
org.fdroid.fdroid.privileged, and this code now also grants it to
com.android.bluetooth. I see no security threat with exposing these
files to both applications, because the .apk files only ever:
 * Were downloaded from the public internet into a (potentially public)
   cache dir.
 * Were sourced from an `ApplicationInfo#publicSourceDir, in which
   case any app can access that anyway.

Fises #837.
This commit is contained in:
Peter Serwylo 2017-05-05 14:40:20 +10:00
parent 2f4b00dc75
commit 09ad7fe3d0
3 changed files with 47 additions and 7 deletions

View File

@ -54,12 +54,14 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProviderService;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.net.ImageLoaderForUIL;
import org.fdroid.fdroid.net.WifiStateChangeService;
import sun.net.www.protocol.bluetooth.Handler;
import java.io.IOException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
@ -381,7 +383,7 @@ public class FDroidApp extends Application {
// The APK type ("application/vnd.android.package-archive") is blocked by stock Android, so use zip
sendBt.setType("application/zip");
sendBt.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + appInfo.publicSourceDir));
sendBt.putExtra(Intent.EXTRA_STREAM, ApkFileProvider.getSafeUri(this, appInfo));
// not all devices have the same Bluetooth Activities, so
// let's find it
@ -397,6 +399,12 @@ public class FDroidApp extends Application {
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not get application info to send via bluetooth", e);
found = false;
} catch (IOException e) {
// This will crash the app, because we want to get error reports of this.
// Additionally, it is not going to happen in the background or other important times
// (like application startup), only when the user actively performs a function (i.e. share
// via bluetooth). As such, it should not be too much of a burden to crash here.
throw new RuntimeException("Error preparing file to send via Bluetooth", e);
}
if (sendBt != null) {

View File

@ -20,6 +20,7 @@
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import com.nostra13.universalimageloader.utils.StorageUtils;
@ -36,6 +37,15 @@ public class ApkCache {
private static final String CACHE_DIR = "apks";
/**
* Same as {@link #copyApkFromCacheToFiles(Context, File, Apk)}, except it does not need to
* verify the hash after copying. This is because we are copying from an installed apk, which
* other apps do not have permission to modify.
*/
public static SanitizedFile copyInstalledApkToFiles(Context context, ApplicationInfo appInfo) throws IOException {
return copyApkToFiles(context, new File(appInfo.publicSourceDir), false, null, null);
}
/**
* Copy the APK to the safe location inside of the protected area
* of the app to prevent attacks based on other apps swapping the file
@ -44,12 +54,24 @@ public class ApkCache {
*/
public static SanitizedFile copyApkFromCacheToFiles(Context context, File apkFile, Apk expectedApk)
throws IOException {
return copyApkToFiles(context, apkFile, true, expectedApk.hash, expectedApk.hashType);
}
/**
* Copy an APK from {@param apkFile} to our internal files directory for 20 minutes.
* @param verifyHash If the file was just downloaded, then you should mark this as true and
* request the file to be verified once it has finished copying. Otherwise,
* if the app was installed from part of the system where it can't be tampered
* with (e.g. installed apks on disk) then
*/
private static SanitizedFile copyApkToFiles(Context context, File apkFile, boolean verifyHash, String hash, String hashType)
throws IOException {
SanitizedFile sanitizedApkFile = SanitizedFile.knownSanitized(
File.createTempFile("install-", ".apk", context.getFilesDir()));
FileUtils.copyFile(apkFile, sanitizedApkFile);
// verify copied file's hash with expected hash from Apk class
if (!Hasher.isFileMatchingHash(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) {
if (verifyHash && !Hasher.isFileMatchingHash(sanitizedApkFile, hash, hashType)) {
FileUtils.deleteQuietly(apkFile);
throw new IOException(apkFile + " failed to verify!");
}

View File

@ -21,7 +21,9 @@ package org.fdroid.fdroid.installer;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import org.fdroid.fdroid.data.Apk;
@ -47,6 +49,11 @@ public class ApkFileProvider extends FileProvider {
private static final String AUTHORITY = "org.fdroid.fdroid.installer.ApkFileProvider";
public static Uri getSafeUri(Context context, ApplicationInfo appInfo) throws IOException {
SanitizedFile tempApkFile = ApkCache.copyInstalledApkToFiles(context, appInfo);
return getSafeUri(context, tempApkFile, Build.VERSION.SDK_INT >= 24);
}
/**
* Copies the APK into private data directory of F-Droid and returns a "file" or "content" Uri
* to be used for installation.
@ -54,14 +61,17 @@ public class ApkFileProvider extends FileProvider {
public static Uri getSafeUri(Context context, Uri localApkUri, Apk expectedApk, boolean useContentUri)
throws IOException {
File apkFile = new File(localApkUri.getPath());
SanitizedFile tempApkFile = ApkCache.copyApkFromCacheToFiles(context, apkFile, expectedApk);
return getSafeUri(context, tempApkFile, useContentUri);
SanitizedFile sanitizedApkFile =
ApkCache.copyApkFromCacheToFiles(context, apkFile, expectedApk);
}
private static Uri getSafeUri(Context context, SanitizedFile tempApkFile, boolean useContentUri) {
if (useContentUri) {
// return a content Uri using support libs FileProvider
Uri apkUri = getUriForFile(context, AUTHORITY, sanitizedApkFile);
Uri apkUri = getUriForFile(context, AUTHORITY, tempApkFile);
context.grantUriPermission("org.fdroid.fdroid.privileged", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.grantUriPermission("com.android.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return apkUri;
}
@ -70,9 +80,9 @@ public class ApkFileProvider extends FileProvider {
// have access is insecure, because apps with permission to write to the external
// storage can overwrite the app between F-Droid asking for it to be installed and
// the installer actually installing it.
sanitizedApkFile.setReadable(true, false);
tempApkFile.setReadable(true, false);
return Uri.fromFile(sanitizedApkFile);
return Uri.fromFile(tempApkFile);
}
}