Merge branch 'apk-tests' into 'master'

ApkVerifier Tests

This are some tests for ApkVerifier. More will follow when we merge https://gitlab.com/fdroid/fdroidserver/merge_requests/150 and implement parsing of permissions with min and max sdk versions.

NOTE: This androidTest cannot run as a Robolectric test because the required methods from PackageManger are not included in Robolectric's Android API.

The corresponding exception by robolectric:
```
org.fdroid.fdroid.installer.ApkVerifierTest > testVerifier FAILED
00:31:18.241 [DEBUG] [TestEventLogger]     java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
00:31:18.241 [DEBUG] [TestEventLogger]         at java.lang.Class.getDeclaredMethods0(Native Method)
00:31:18.241 [DEBUG] [TestEventLogger]         at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
00:31:18.241 [DEBUG] [TestEventLogger]         at java.lang.Class.getDeclaredMethod(Class.java:2128)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.robolectric.util.ReflectionHelpers.callStaticMethod(ReflectionHelpers.java:224)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.robolectric.internal.bytecode.RobolectricInternals.performStaticInitialization(RobolectricInternals.java:54)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.robolectric.internal.bytecode.ShadowWrangler.classInitializing(ShadowWrangler.java:119)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.robolectric.internal.bytecode.RobolectricInternals.classInitializing(RobolectricInternals.java:18)
00:31:18.241 [DEBUG] [TestEventLogger]         at android.content.pm.PackageParser.<clinit>(PackageParser.java)
00:31:18.241 [DEBUG] [TestEventLogger]         at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.fdroid.fdroid.installer.ApkVerifier.verifyApk(ApkVerifier.java:56)
00:31:18.241 [DEBUG] [TestEventLogger]         at org.fdroid.fdroid.installer.ApkVerifierTest.testVerifier(ApkVerifierTest.java:78)
00:31:18.242 [DEBUG] [TestEventLogger]         at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
[...]
00:31:18.244 [DEBUG] [TestEventLogger] 
00:31:18.244 [DEBUG] [TestEventLogger]         Caused by:
00:31:18.245 [DEBUG] [TestEventLogger]         java.lang.ClassNotFoundException: java.util.jar.StrictJarFile
00:31:18.245 [DEBUG] [TestEventLogger]             at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
00:31:18.245 [DEBUG] [TestEventLogger]             at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
00:31:18.245 [DEBUG] [TestEventLogger]             at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
00:31:18.245 [DEBUG] [TestEventLogger]             at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.robolectric.internal.bytecode.InstrumentingClassLoader.loadClass(InstrumentingClassLoader.java:124)
00:31:18.245 [DEBUG] [TestEventLogger]             at java.lang.Class.getDeclaredMethods0(Native Method)
00:31:18.245 [DEBUG] [TestEventLogger]             at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
00:31:18.245 [DEBUG] [TestEventLogger]             at java.lang.Class.getDeclaredMethod(Class.java:2128)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.robolectric.util.ReflectionHelpers.callStaticMethod(ReflectionHelpers.java:224)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.robolectric.internal.bytecode.RobolectricInternals.performStaticInitialization(RobolectricInternals.java:54)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.robolectric.internal.bytecode.ShadowWrangler.classInitializing(ShadowWrangler.java:119)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.robolectric.internal.bytecode.RobolectricInternals.classInitializing(RobolectricInternals.java:18)
00:31:18.245 [DEBUG] [TestEventLogger]             at android.content.pm.PackageParser.<clinit>(PackageParser.java)
00:31:18.245 [DEBUG] [TestEventLogger]             at android.content.pm.PackageManager.$$robo$$getPackageArchiveInfo(PackageManager.java:3545)
00:31:18.245 [DEBUG] [TestEventLogger]             at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java)
00:31:18.245 [DEBUG] [TestEventLogger]             at org.fdroid.fdroid.installer.ApkVerifier.verifyApk(ApkVerifier.java:56)
00:31:18.246 [DEBUG] [TestEventLogger]             at org.fdroid.fdroid.installer.ApkVerifierTest.testVerifier(ApkVerifierTest.java:78)
00:31:18.246 [DEBUG] [TestEventLogger]             at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[...]
```

See merge request !367
This commit is contained in:
Hans-Christoph Steiner 2016-08-12 12:38:24 +00:00
commit fdec402837
8 changed files with 296 additions and 31 deletions

View File

@ -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;
}
}

View File

@ -4,12 +4,10 @@ import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4; 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.fdroid.fdroid.data.SanitizedFile;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -17,10 +15,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.File; 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 java.util.UUID;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -36,8 +30,6 @@ import static org.junit.Assume.assumeTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class FileCompatTest { public class FileCompatTest {
private static final String TAG = "FileCompatTest";
private SanitizedFile sourceFile; private SanitizedFile sourceFile;
private SanitizedFile destFile; private SanitizedFile destFile;
@ -45,7 +37,8 @@ public class FileCompatTest {
public void setUp() { public void setUp() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
File dir = getWriteableDir(instrumentation); 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"); destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct");
assertFalse(destFile.exists()); assertFalse(destFile.exists());
assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists()); assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists());
@ -82,26 +75,6 @@ public class FileCompatTest {
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists()); 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, * Prefer internal over external storage, because external tends to be FAT filesystems,

View File

@ -0,0 +1,242 @@
/*
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.
* <p/>
* 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();
}
}
}

View File

@ -45,6 +45,10 @@ class ApkVerifier {
private final Apk expectedApk; private final Apk expectedApk;
private final PackageManager pm; 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) { ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) {
this.localApkUri = localApkUri; this.localApkUri = localApkUri;
this.expectedApk = expectedApk; this.expectedApk = expectedApk;
@ -52,11 +56,18 @@ class ApkVerifier {
} }
public void verifyApk() throws ApkVerificationException, ApkPermissionUnequalException { public void verifyApk() throws ApkVerificationException, ApkPermissionUnequalException {
Utils.debugLog(TAG, "localApkUri.getPath: " + localApkUri.getPath());
// parse downloaded apk file locally // parse downloaded apk file locally
PackageInfo localApkInfo = pm.getPackageArchiveInfo( PackageInfo localApkInfo = pm.getPackageArchiveInfo(
localApkUri.getPath(), PackageManager.GET_PERMISSIONS); localApkUri.getPath(), PackageManager.GET_PERMISSIONS);
if (localApkInfo == null) { 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 // check if the apk has the expected packageName