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
8 changed files with 296 additions and 31 deletions

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

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.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,

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 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