diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 3dabde160..c543aa2d2 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java index c920ba13d..96bbc341a 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java @@ -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!"); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java index aac9fe166..1ccda4656 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java @@ -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); } }