diff --git a/app/src/androidTest/assets/org.fdroid.permissions.minmax.apk b/app/src/androidTest/assets/org.fdroid.permissions.minmax.apk new file mode 100644 index 000000000..880342cc6 Binary files /dev/null and b/app/src/androidTest/assets/org.fdroid.permissions.minmax.apk differ diff --git a/app/src/androidTest/assets/org.fdroid.permissions.minmax.zip b/app/src/androidTest/assets/org.fdroid.permissions.minmax.zip new file mode 100644 index 000000000..068ad82fd Binary files /dev/null and b/app/src/androidTest/assets/org.fdroid.permissions.minmax.zip differ diff --git a/app/src/androidTest/assets/org.fdroid.permissions.sdk14.apk b/app/src/androidTest/assets/org.fdroid.permissions.sdk14.apk new file mode 100644 index 000000000..046b8ddf4 Binary files /dev/null and b/app/src/androidTest/assets/org.fdroid.permissions.sdk14.apk differ diff --git a/app/src/androidTest/assets/org.fdroid.permissions.sdk14.zip b/app/src/androidTest/assets/org.fdroid.permissions.sdk14.zip new file mode 100644 index 000000000..3ab3d0f59 Binary files /dev/null and b/app/src/androidTest/assets/org.fdroid.permissions.sdk14.zip differ diff --git a/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java b/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java new file mode 100644 index 000000000..6d5265dfb --- /dev/null +++ b/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java @@ -0,0 +1,39 @@ +package org.fdroid.fdroid; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static org.junit.Assert.fail; + +public class AssetUtils { + + private static final String TAG = "Utils"; + + @Nullable + public static File copyAssetToDir(Context context, String assetName, File directory) { + File tempFile = null; + InputStream input = null; + OutputStream output = null; + try { + tempFile = File.createTempFile(assetName, ".testasset", directory); + Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory); + input = context.getAssets().open(assetName); + output = new FileOutputStream(tempFile); + Utils.copy(input, output); + } catch (IOException e) { + fail(e.getMessage()); + } finally { + Utils.closeQuietly(output); + Utils.closeQuietly(input); + } + return tempFile; + } + +} diff --git a/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java b/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java index f65c28b46..77eac511a 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java @@ -4,12 +4,10 @@ import android.app.Instrumentation; import android.content.Context; import android.os.Build; import android.os.Environment; -import android.support.annotation.Nullable; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; -import android.util.Log; -import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.AssetUtils; import org.fdroid.fdroid.data.SanitizedFile; import org.junit.After; import org.junit.Before; @@ -17,10 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.util.UUID; import static org.junit.Assert.assertFalse; @@ -36,8 +30,6 @@ import static org.junit.Assume.assumeTrue; @RunWith(AndroidJUnit4.class) public class FileCompatTest { - private static final String TAG = "FileCompatTest"; - private SanitizedFile sourceFile; private SanitizedFile destFile; @@ -45,7 +37,8 @@ public class FileCompatTest { public void setUp() { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); File dir = getWriteableDir(instrumentation); - sourceFile = SanitizedFile.knownSanitized(copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir)); + sourceFile = SanitizedFile.knownSanitized( + AssetUtils.copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir)); destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct"); assertFalse(destFile.exists()); assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists()); @@ -82,26 +75,6 @@ public class FileCompatTest { assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists()); } - @Nullable - private static File copyAssetToDir(Context context, String assetName, File directory) { - File tempFile; - InputStream input = null; - OutputStream output = null; - try { - tempFile = File.createTempFile(assetName + "-", ".testasset", directory); - Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory); - input = context.getAssets().open(assetName); - output = new FileOutputStream(tempFile); - Utils.copy(input, output); - } catch (IOException e) { - e.printStackTrace(); - return null; - } finally { - Utils.closeQuietly(output); - Utils.closeQuietly(input); - } - return tempFile; - } /** * Prefer internal over external storage, because external tends to be FAT filesystems, diff --git a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java new file mode 100644 index 000000000..ecfa3047d --- /dev/null +++ b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.installer; + +import android.app.Instrumentation; +import android.net.Uri; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.fdroid.fdroid.AssetUtils; +import org.fdroid.fdroid.data.Apk; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml + * and checking the listed permissions against the ones specified in apks' AndroidManifest, + * which have been specifically generated for this test. + * - the apk file name must match the package name in the xml + * - the versionName of listed apks inside the repo have either a good or bad outcome. + * this must be defined in GOOD_VERSION_NAMES and BAD_VERSION_NAMES. + *

+ * NOTE: This androidTest cannot run as a Robolectric test because the + * required methods from PackageManger are not included in Robolectric's Android API. + * java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile + * at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545) + */ +@RunWith(AndroidJUnit4.class) +public class ApkVerifierTest { + + Instrumentation instrumentation; + + File sdk14Apk; + File minMaxApk; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void setUp() { + instrumentation = InstrumentationRegistry.getInstrumentation(); + File dir = null; + try { + dir = tempFolder.newFolder("apks"); + } catch (IOException e) { + fail(e.getMessage()); + } + sdk14Apk = AssetUtils.copyAssetToDir(instrumentation.getContext(), + "org.fdroid.permissions.sdk14.apk", + dir + ); + minMaxApk = AssetUtils.copyAssetToDir(instrumentation.getContext(), + "org.fdroid.permissions.minmax.apk", + dir + ); + assertTrue(sdk14Apk.exists()); + assertTrue(minMaxApk.exists()); + } + + @Test + public void testWithoutPrefix() { + Apk apk = new Apk(); + apk.packageName = "org.fdroid.permissions.sdk14"; + apk.targetSdkVersion = 14; + apk.permissions = new String[]{ + "AUTHENTICATE_ACCOUNTS", + "MANAGE_ACCOUNTS", + "READ_PROFILE", + "WRITE_PROFILE", + "GET_ACCOUNTS", + "READ_CONTACTS", + "WRITE_CONTACTS", + "WRITE_EXTERNAL_STORAGE", + "READ_EXTERNAL_STORAGE", + "INTERNET", + "ACCESS_NETWORK_STATE", + "NFC", + "READ_SYNC_SETTINGS", + "WRITE_SYNC_SETTINGS", + "WRITE_CALL_LOG", // implied-permission! + "READ_CALL_LOG", // implied-permission! + }; + + Uri uri = Uri.fromFile(sdk14Apk); + + ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); + + try { + apkVerifier.verifyApk(); + } catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + @Test + public void testWithPrefix() { + Apk apk = new Apk(); + apk.packageName = "org.fdroid.permissions.sdk14"; + apk.targetSdkVersion = 14; + apk.permissions = new String[]{ + "android.permission.AUTHENTICATE_ACCOUNTS", + "android.permission.MANAGE_ACCOUNTS", + "android.permission.READ_PROFILE", + "android.permission.WRITE_PROFILE", + "android.permission.GET_ACCOUNTS", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.NFC", + "android.permission.READ_SYNC_SETTINGS", + "android.permission.WRITE_SYNC_SETTINGS", + "android.permission.WRITE_CALL_LOG", // implied-permission! + "android.permission.READ_CALL_LOG", // implied-permission! + }; + + Uri uri = Uri.fromFile(sdk14Apk); + + ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); + + try { + apkVerifier.verifyApk(); + } catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + /** + * Additional permissions are okay. The user is simply + * warned about a permission that is not used inside the apk + */ + @Test + public void testAdditionalPermission() { + Apk apk = new Apk(); + apk.packageName = "org.fdroid.permissions.sdk14"; + apk.targetSdkVersion = 14; + apk.permissions = new String[]{ + "android.permission.AUTHENTICATE_ACCOUNTS", + "android.permission.MANAGE_ACCOUNTS", + "android.permission.READ_PROFILE", + "android.permission.WRITE_PROFILE", + "android.permission.GET_ACCOUNTS", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.NFC", + "android.permission.READ_SYNC_SETTINGS", + "android.permission.WRITE_SYNC_SETTINGS", + "android.permission.WRITE_CALL_LOG", // implied-permission! + "android.permission.READ_CALL_LOG", // implied-permission! + "NEW_PERMISSION", + }; + + Uri uri = Uri.fromFile(sdk14Apk); + + ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); + + try { + apkVerifier.verifyApk(); + } catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + /** + * Missing permissions are not okay! + * The user is then not warned about a permission that the apk uses! + */ + @Test + public void testMissingPermission() { + Apk apk = new Apk(); + apk.packageName = "org.fdroid.permissions.sdk14"; + apk.targetSdkVersion = 14; + apk.permissions = new String[]{ + //"android.permission.AUTHENTICATE_ACCOUNTS", + "android.permission.MANAGE_ACCOUNTS", + "android.permission.READ_PROFILE", + "android.permission.WRITE_PROFILE", + "android.permission.GET_ACCOUNTS", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.NFC", + "android.permission.READ_SYNC_SETTINGS", + "android.permission.WRITE_SYNC_SETTINGS", + "android.permission.WRITE_CALL_LOG", // implied-permission! + "android.permission.READ_CALL_LOG", // implied-permission! + }; + + Uri uri = Uri.fromFile(sdk14Apk); + + ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); + + try { + apkVerifier.verifyApk(); + fail(); + } catch (ApkVerifier.ApkVerificationException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (ApkVerifier.ApkPermissionUnequalException e) { + e.printStackTrace(); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java index ebcd56b8d..76795df29 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java @@ -45,6 +45,10 @@ class ApkVerifier { private final Apk expectedApk; private final PackageManager pm; + /** + * IMPORTANT: localApkUri must be available as a File on the file system with an absolute path + * to be readable by Android's internal PackageParser. + */ ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) { this.localApkUri = localApkUri; this.expectedApk = expectedApk; @@ -52,11 +56,18 @@ class ApkVerifier { } public void verifyApk() throws ApkVerificationException, ApkPermissionUnequalException { + Utils.debugLog(TAG, "localApkUri.getPath: " + localApkUri.getPath()); + // parse downloaded apk file locally PackageInfo localApkInfo = pm.getPackageArchiveInfo( localApkUri.getPath(), PackageManager.GET_PERMISSIONS); if (localApkInfo == null) { - throw new ApkVerificationException("Parsing apk file failed!"); + // Unfortunately, more specific errors are not forwarded to us + // but the internal PackageParser sometimes shows warnings in logcat such as + // "Requires newer sdk version #14 (current version is #11)" + throw new ApkVerificationException("Parsing apk file failed!" + + "Maybe minSdk of apk is lower than current Sdk?" + + "Look into logcat for more specific warnings of Android's PackageParser"); } // check if the apk has the expected packageName