support extended 'uses-permissions' tags in APKs

<uses-permissions/> tags can have min and max SDK to take effect.  This is
not supported currently, and it necessary especially with the privileged
installer so it can properly represent the permissions that an APK is
requesting.

For example:
<uses-permission
  android:name="android.permission.MANAGE_ACCOUNTS"
  android:maxSdkVersion="22" />
<uses-permission-sdk-23
  android:name="android.permission.CAMERA" />
<uses-permission-sdk-23
  android:name="android.permission.CALL_PHONE"
  android:maxSdkVersion="23" />
This commit is contained in:
Hans-Christoph Steiner 2016-10-10 11:26:04 +02:00
parent 2350b4e694
commit 6f0c9ff88a
9 changed files with 570 additions and 79 deletions

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<fdroid>
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
<description>
This is just a test of the extended permissions attributes.
</description>
</repo>
<application id="urzip.at.or.at.urzip">
<id>at.bitfire.davdroid</id>
<added>2013-10-13</added>
<lastupdated>2016-06-26</lastupdated>
<name>DAVdroid</name>
<summary>Contacts and Calendar sync</summary>
<icon>at.bitfire.davdroid.107.png</icon>
<desc>apk generated from urzip to test extended permissions</desc>
<license>GPLv3</license>
<categories>Internet</categories>
<category>Internet</category>
<web>https://davdroid.bitfire.at/</web>
<source>https://davdroid.bitfire.at/source/</source>
<tracker>https://davdroid.bitfire.at/forums/</tracker>
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
<donate>https://davdroid.bitfire.at/donate/</donate>
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
<flattr>2100160</flattr>
<marketversion>1.1.1.2</marketversion>
<marketvercode>107</marketvercode>
<package>
<version>1.3.2-FAKE</version>
<versioncode>117</versioncode>
<apkname>org.fdroid.extendedpermissionstest.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-09-22</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,READ_SYNC_STATS
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission-sdk-23 name="android.permission.CAMERA" />
<uses-permission-sdk-23 name="android.permission.CALL_PHONE" maxSdkVersion="23" />
</package>
<package>
<version>1.3.1-ose</version>
<versioncode>116</versioncode>
<apkname>at.bitfire.davdroid_116.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-21</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.1.1.2</version>
<versioncode>107</versioncode>
<apkname>at.bitfire.davdroid_107.apk</apkname>
<srcname>at.bitfire.davdroid_107_src.tar.gz</srcname>
<hash type="sha256">9a616f2e97bf8cf012baf896f95667dea4e3ce3252b31c5715073638a9fcc3d4
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3134363</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-26</added>
<permissions>
org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
</permissions>
</package>
<package>
<version>1.1.1.1</version>
<versioncode>105</versioncode>
<apkname>at.bitfire.davdroid_105.apk</apkname>
<srcname>at.bitfire.davdroid_105_src.tar.gz</srcname>
<hash type="sha256">4a0408c61536a1cc1028cea4273adbde2e57dfa2b12d93c3b52f4c3d095e2849
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3131567</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-24</added>
<permissions>
org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
</permissions>
</package>
<package>
<version>1.1.1</version>
<versioncode>104</versioncode>
<apkname>at.bitfire.davdroid_104.apk</apkname>
<srcname>at.bitfire.davdroid_104_src.tar.gz</srcname>
<hash type="sha256">09ba34996177efe8b1498a93fe6521ab84efab3bccb0f42449116e80b59e5b56
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3131367</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-22</added>
<permissions>
org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
</permissions>
</package>
</application>
</fdroid>

View File

@ -21,18 +21,32 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
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.AssetUtils; import org.fdroid.fdroid.AssetUtils;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest; import org.fdroid.fdroid.compat.FileCompatTest;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.mock.RepoDetails;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -40,10 +54,7 @@ import static org.junit.Assert.fail;
* This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml * This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
* and checking the listed permissions against the ones specified in apks' AndroidManifest, * and checking the listed permissions against the ones specified in apks' AndroidManifest,
* which have been specifically generated for this test. * which have been specifically generated for this test.
* - the apk file name must match the package name in the xml * <p>
* - 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 * NOTE: This androidTest cannot run as a Robolectric test because the
* required methods from PackageManger are not included in Robolectric's Android API. * required methods from PackageManger are not included in Robolectric's Android API.
* java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile * java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
@ -51,11 +62,14 @@ import static org.junit.Assert.fail;
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ApkVerifierTest { public class ApkVerifierTest {
public static final String TAG = "ApkVerifierTest";
Instrumentation instrumentation; Instrumentation instrumentation;
File sdk14Apk; File sdk14Apk;
File minMaxApk; File minMaxApk;
private File extendedPermissionsApk;
private File extendedPermsXml;
@Before @Before
public void setUp() { public void setUp() {
@ -71,8 +85,18 @@ public class ApkVerifierTest {
"org.fdroid.permissions.minmax.apk", "org.fdroid.permissions.minmax.apk",
dir dir
); );
extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
"org.fdroid.extendedpermissionstest.apk",
dir
);
extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(),
"extendedPerms.xml",
dir
);
assertTrue(sdk14Apk.exists()); assertTrue(sdk14Apk.exists());
assertTrue(minMaxApk.exists()); assertTrue(minMaxApk.exists());
assertTrue(extendedPermissionsApk.exists());
assertTrue(extendedPermsXml.exists());
} }
@Test @Test
@ -80,7 +104,7 @@ public class ApkVerifierTest {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
apk.requestedPermissions = new String[]{ String[] noPrefixPermissions = new String[]{
"AUTHENTICATE_ACCOUNTS", "AUTHENTICATE_ACCOUNTS",
"MANAGE_ACCOUNTS", "MANAGE_ACCOUNTS",
"READ_PROFILE", "READ_PROFILE",
@ -98,9 +122,12 @@ public class ApkVerifierTest {
"WRITE_CALL_LOG", // implied-permission! "WRITE_CALL_LOG", // implied-permission!
"READ_CALL_LOG", // implied-permission! "READ_CALL_LOG", // implied-permission!
}; };
for (int i = 0; i < noPrefixPermissions.length; i++) {
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
}
apk.requestedPermissions = noPrefixPermissions;
Uri uri = Uri.fromFile(sdk14Apk); Uri uri = Uri.fromFile(sdk14Apk);
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
try { try {
@ -111,6 +138,31 @@ public class ApkVerifierTest {
} }
} }
@Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
public void testWithMinMax()
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.minmax";
apk.targetSdkVersion = 24;
ArrayList<String> permissionsList = new ArrayList<>();
permissionsList.add("android.permission.READ_CALENDAR");
if (Build.VERSION.SDK_INT <= 18) {
permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
}
if (Build.VERSION.SDK_INT >= 23) {
permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
}
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
Uri uri = Uri.fromFile(minMaxApk);
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
apkVerifier.verifyApk();
permissionsList.add("ADDITIONAL_PERMISSION");
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
apkVerifier.verifyApk();
}
@Test @Test
public void testWithPrefix() { public void testWithPrefix() {
Apk apk = new Apk(); Apk apk = new Apk();
@ -151,8 +203,9 @@ public class ApkVerifierTest {
* Additional permissions are okay. The user is simply * Additional permissions are okay. The user is simply
* warned about a permission that is not used inside the apk * warned about a permission that is not used inside the apk
*/ */
@Test @Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
public void testAdditionalPermission() { public void testAdditionalPermission()
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
@ -173,19 +226,12 @@ public class ApkVerifierTest {
"android.permission.WRITE_SYNC_SETTINGS", "android.permission.WRITE_SYNC_SETTINGS",
"android.permission.WRITE_CALL_LOG", // implied-permission! "android.permission.WRITE_CALL_LOG", // implied-permission!
"android.permission.READ_CALL_LOG", // implied-permission! "android.permission.READ_CALL_LOG", // implied-permission!
"NEW_PERMISSION", "android.permission.FAKE_NEW_PERMISSION",
}; };
Uri uri = Uri.fromFile(sdk14Apk); Uri uri = Uri.fromFile(sdk14Apk);
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
try {
apkVerifier.verifyApk(); apkVerifier.verifyApk();
} catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
e.printStackTrace();
fail(e.getMessage());
}
} }
/** /**
@ -231,4 +277,73 @@ public class ApkVerifierTest {
} }
} }
@Test
public void testExtendedPerms() throws IOException,
ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
RepoDetails actualDetails = getFromFile(extendedPermsXml);
HashSet<String> expectedSet = new HashSet<>(Arrays.asList(new String[]{
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE",
"android.permission.INTERNET",
"android.permission.READ_SYNC_STATS",
"android.permission.READ_SYNC_SETTINGS",
"android.permission.WRITE_SYNC_SETTINGS",
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
"android.permission.READ_CONTACTS",
"android.permission.WRITE_CONTACTS",
"android.permission.READ_CALENDAR",
"android.permission.WRITE_CALENDAR",
}));
if (Build.VERSION.SDK_INT <= 18) {
expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
}
if (Build.VERSION.SDK_INT <= 22) {
expectedSet.add("android.permission.GET_ACCOUNTS");
expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
expectedSet.add("android.permission.MANAGE_ACCOUNTS");
}
if (Build.VERSION.SDK_INT >= 23) {
expectedSet.add("android.permission.CAMERA");
if (Build.VERSION.SDK_INT <= 23) {
expectedSet.add("android.permission.CALL_PHONE");
}
}
Apk apk = actualDetails.apks.get(0);
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
for (String permission : expectedSet) {
if (!actualSet.contains(permission)) {
Log.i(TAG, permission + " in expected but not actual! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
for (String permission : actualSet) {
if (!expectedSet.contains(permission)) {
Log.i(TAG, permission + " in actual but not expected! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
badPermissions[badPermissions.length - 1] = "notarealpermission";
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
Uri uri = Uri.fromFile(extendedPermissionsApk);
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
apkVerifier.verifyApk();
}
@NonNull
private RepoDetails getFromFile(File indexFile) throws IOException {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(indexFile);
return RepoDetails.getFromFile(inputStream, Repo.PUSH_REQUEST_IGNORE);
} finally {
Utils.closeQuietly(inputStream);
}
}
} }

View File

@ -19,6 +19,7 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -32,7 +33,9 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
/** /**
* Parses the index.xml into Java data structures. * Parses the index.xml into Java data structures.
@ -57,7 +60,14 @@ public class RepoXMLHandler extends DefaultHandler {
private String repoDescription; private String repoDescription;
private String repoName; private String repoName;
// the X.509 signing certificate stored in the header of index.xml /**
* Set of requested permissions per package/APK
*/
private final HashSet<String> requestedPermissionsSet = new HashSet<>();
/**
* the X.509 signing certificate stored in the header of index.xml
*/
private String repoSigningCert; private String repoSigningCert;
private final StringBuilder curchars = new StringBuilder(); private final StringBuilder curchars = new StringBuilder();
@ -89,6 +99,9 @@ public class RepoXMLHandler extends DefaultHandler {
if ("application".equals(localName) && curapp != null) { if ("application".equals(localName) && curapp != null) {
onApplicationParsed(); onApplicationParsed();
} else if ("package".equals(localName) && curapk != null && curapp != null) { } else if ("package".equals(localName) && curapk != null && curapp != null) {
int size = requestedPermissionsSet.size();
curapk.requestedPermissions = requestedPermissionsSet.toArray(new String[size]);
requestedPermissionsSet.clear();
apksList.add(curapk); apksList.add(curapk);
curapk = null; curapk = null;
} else if ("repo".equals(localName)) { } else if ("repo".equals(localName)) {
@ -157,8 +170,8 @@ public class RepoXMLHandler extends DefaultHandler {
case ApkTable.Cols.ADDED_DATE: case ApkTable.Cols.ADDED_DATE:
curapk.added = Utils.parseDate(str, null); curapk.added = Utils.parseDate(str, null);
break; break;
case ApkTable.Cols.REQUESTED_PERMISSIONS: case "permissions": // together with <uses-permissions* makes ApkTable.Cols.REQUESTED_PERMISSIONS
curapk.requestedPermissions = Utils.parseCommaSeparatedString(str); addCommaSeparatedPermissions(str);
break; break;
case ApkTable.Cols.FEATURES: case ApkTable.Cols.FEATURES:
curapk.features = Utils.parseCommaSeparatedString(str); curapk.features = Utils.parseCommaSeparatedString(str);
@ -248,6 +261,42 @@ public class RepoXMLHandler extends DefaultHandler {
} }
} }
private static final Pattern OLD_FDROID_PERMISSION = Pattern.compile("[A-Z_]+");
/**
* It appears that the default Android permissions in android.Manifest.permissions
* are prefixed with "android.permission." and then the constant name.
* FDroid just includes the constant name in the apk list, so we prefix it
* with "android.permission."
*
* @see <a href="https://gitlab.com/fdroid/fdroidserver/blob/1afa8cfc/update.py#L91">
* More info into index - size, permissions, features, sdk version</a>
*/
public static String fdroidToAndroidPermission(String permission) {
if (OLD_FDROID_PERMISSION.matcher(permission).matches()) {
return "android.permission." + permission;
}
return permission;
}
private void addRequestedPermission(String permission) {
requestedPermissionsSet.add(permission);
}
private void addCommaSeparatedPermissions(String permissions) {
String[] array = Utils.parseCommaSeparatedString(permissions);
if (array != null) {
for (String permission : array) {
requestedPermissionsSet.add(fdroidToAndroidPermission(permission));
}
}
}
private void removeRequestedPermission(String permission) {
requestedPermissionsSet.remove(permission);
}
private void onApplicationParsed() { private void onApplicationParsed() {
receiver.receiveApp(curapp, apksList); receiver.receiveApp(curapp, apksList);
curapp = null; curapp = null;
@ -308,6 +357,24 @@ public class RepoXMLHandler extends DefaultHandler {
} else if ("hash".equals(localName) && curapk != null) { } else if ("hash".equals(localName) && curapk != null) {
currentApkHashType = attributes.getValue("", "type"); currentApkHashType = attributes.getValue("", "type");
} else if ("uses-permission".equals(localName) && curapk != null) {
String maxSdkVersion = attributes.getValue("maxSdkVersion");
if (maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion)) {
addRequestedPermission(attributes.getValue("name"));
} else {
removeRequestedPermission(attributes.getValue("name"));
}
} else if ("uses-permission-sdk-23".equals(localName) && curapk != null) {
String maxSdkVersion = attributes.getValue("maxSdkVersion");
if (Build.VERSION.SDK_INT >= 23 &&
(maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion))) {
addRequestedPermission(attributes.getValue("name"));
} else {
removeRequestedPermission(attributes.getValue("name"));
}
} else if ("uses-feature".equals(localName) && curapk != null) {
System.out.println("TODO startElement " + uri + " " + localName + " " + qName);
// TODO
} }
curchars.setLength(0); curchars.setLength(0);
} }

View File

@ -7,11 +7,11 @@ import android.os.Build;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
@ -132,7 +132,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
apkName = cursor.getString(i); apkName = cursor.getString(i);
break; break;
case Cols.REQUESTED_PERMISSIONS: case Cols.REQUESTED_PERMISSIONS:
requestedPermissions = Utils.parseCommaSeparatedString(cursor.getString(i)); requestedPermissions = convertToRequestedPermissions(cursor.getString(i));
break; break;
case Cols.NATIVE_CODE: case Cols.NATIVE_CODE:
nativecode = Utils.parseCommaSeparatedString(cursor.getString(i)); nativecode = Utils.parseCommaSeparatedString(cursor.getString(i));
@ -235,44 +235,6 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
return new File(App.getObbDir(packageName), obbPatchFile); return new File(App.getObbDir(packageName), obbPatchFile);
} }
public ArrayList<String> getFullPermissionList() {
if (this.requestedPermissions == null) {
return new ArrayList<>();
}
ArrayList<String> permissionsFull = new ArrayList<>();
for (String perm : this.requestedPermissions) {
permissionsFull.add(fdroidToAndroidPermission(perm));
}
return permissionsFull;
}
public String[] getFullPermissionsArray() {
ArrayList<String> fullPermissions = getFullPermissionList();
return fullPermissions.toArray(new String[fullPermissions.size()]);
}
public HashSet<String> getFullPermissionsSet() {
return new HashSet<>(getFullPermissionList());
}
/**
* It appears that the default Android permissions in android.Manifest.permissions
* are prefixed with "android.permission." and then the constant name.
* FDroid just includes the constant name in the apk list, so we prefix it
* with "android.permission."
*
* @see <a href="https://gitlab.com/fdroid/fdroidserver/blob/1afa8cfc/update.py#L91">
* More info into index - size, permissions, features, sdk version</a>
*/
private static String fdroidToAndroidPermission(String permission) {
if (!permission.contains(".")) {
return "android.permission." + permission;
}
return permission;
}
@Override @Override
public String toString() { public String toString() {
return toContentValues().toString(); return toContentValues().toString();
@ -393,4 +355,17 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
return new Apk[size]; return new Apk[size];
} }
}; };
private String[] convertToRequestedPermissions(String permissionsFromDb) {
String[] array = Utils.parseCommaSeparatedString(permissionsFromDb);
if (array != null) {
HashSet<String> requestedPermissionsSet = new HashSet<>();
for (String permission : array) {
requestedPermissionsSet.add(RepoXMLHandler.fdroidToAndroidPermission(permission));
}
return requestedPermissionsSet.toArray(new String[requestedPermissionsSet.size()]);
}
return null;
}
} }

View File

@ -80,18 +80,10 @@ class ApkVerifier {
} }
// verify permissions, important for unattended installer // verify permissions, important for unattended installer
HashSet<String> localPermissions = getLocalPermissionsSet(localApkInfo); Utils.debugLog(TAG, "localPermissions: " + TextUtils.join("\n", localApkInfo.requestedPermissions));
HashSet<String> expectedPermissions = expectedApk.getFullPermissionsSet(); Utils.debugLog(TAG, "expectedPermissions: " + TextUtils.join("\n", expectedApk.requestedPermissions));
Utils.debugLog(TAG, "localPermissions: " + localPermissions); if (!requestedPermissionsEqual(expectedApk.requestedPermissions, localApkInfo.requestedPermissions)) {
Utils.debugLog(TAG, "expectedPermissions: " + expectedPermissions); throw new ApkPermissionUnequalException("Permissions in APK and index.xml do not match!");
// NOTE: Some permissions could have a maxSdkVersion < current sdk version
// and are thus not parsed by pm.getPackageArchiveInfo().
// Thus, containsAll() instead of equals() is used!
// See also https://gitlab.com/fdroid/fdroidclient/issues/703
if (!expectedPermissions.containsAll(localPermissions)) {
throw new ApkPermissionUnequalException(
"Permissions of the apk file are not a true subset of the permissions listed by the repo," +
" i.e., some permissions have not been shown to the user!");
} }
int localTargetSdkVersion = localApkInfo.applicationInfo.targetSdkVersion; int localTargetSdkVersion = localApkInfo.applicationInfo.targetSdkVersion;
@ -106,13 +98,24 @@ class ApkVerifier {
} }
} }
private HashSet<String> getLocalPermissionsSet(PackageInfo localApkInfo) { /**
String[] localPermissions = localApkInfo.requestedPermissions; * Compares to sets of APK permissions to see if they are an exact match. The
if (localPermissions == null) { * data format is {@link String} arrays but they are in effect sets. This is the
return new HashSet<>(); * same data format as {@link android.content.pm.PackageInfo#requestedPermissions}
*/
public static boolean requestedPermissionsEqual(String[] expected, String[] actual) {
if (expected == null && actual == null) {
return true;
} }
if (expected == null || actual == null) {
return new HashSet<>(Arrays.asList(localApkInfo.requestedPermissions)); return false;
}
if (expected.length != actual.length) {
return false;
}
HashSet<String> expectedSet = new HashSet<>(Arrays.asList(expected));
HashSet<String> actualSet = new HashSet<>(Arrays.asList(actual));
return expectedSet.equals(actualSet);
} }
public static class ApkVerificationException extends Exception { public static class ApkVerificationException extends Exception {

View File

@ -39,7 +39,7 @@ public class AppDiff {
pkgInfo = new PackageInfo(); pkgInfo = new PackageInfo();
pkgInfo.packageName = apk.packageName; pkgInfo.packageName = apk.packageName;
pkgInfo.applicationInfo = new ApplicationInfo(); pkgInfo.applicationInfo = new ApplicationInfo();
pkgInfo.requestedPermissions = apk.getFullPermissionsArray(); pkgInfo.requestedPermissions = apk.requestedPermissions;
init(); init();
} }

View File

@ -69,6 +69,17 @@ public class RepoXMLHandlerTest {
ShadowLog.stream = System.out; ShadowLog.stream = System.out;
} }
@Test
public void testExtendedPerms() throws IOException {
Repo expectedRepo = new Repo();
expectedRepo.name = "F-Droid";
expectedRepo.signingCertificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef";
expectedRepo.description = "This is just a test of the extended permissions attributes.";
expectedRepo.timestamp = 1467169032;
RepoDetails actualDetails = getFromFile("extendedPerms.xml");
handlerTestSuite(expectedRepo, actualDetails, 2, 6, 14, 16);
}
@Test @Test
public void testObbIndex() throws IOException { public void testObbIndex() throws IOException {
writeResourceToObbDir("main.1101613.obb.main.twoversions.obb"); writeResourceToObbDir("main.1101613.obb.main.twoversions.obb");

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<fdroid>
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
<description>
This is just a test of the extended permissions attributes.
</description>
</repo>
<application id="at.bitfire.davdroid">
<id>at.bitfire.davdroid</id>
<added>2013-10-13</added>
<lastupdated>2016-09-21</lastupdated>
<name>DAVdroid</name>
<summary>Contacts and Calendar sync</summary>
<icon>at.bitfire.davdroid.116.png</icon>
<desc>
<p>DAVdroid is a CalDAV/CardDAV synchronisation adapter for Android 4+ devices.</p>
<p>Use it with your own server (like<a href="https://owncloud.org/">OwnCloud</a>,<a
href="http://baikal-server.com/">Baïkal</a>,
<a href="http://www.davical.org/">DAViCal</a>
or<a href="http://radicale.org/">radiCALe</a>) or with a trusted hoster to keep your
contacts and events under your control.
</p>
<p>Integrates natively in Android calendar/contact apps. See homepage for configuration
details, including info about self-signed certificates.
</p>
<p>For a comparison of server software, see the<a
href="https://wiki.debian.org/Groupware">
Debian wiki</a>.
</p>
</desc>
<license>GPLv3</license>
<categories>Internet</categories>
<category>Internet</category>
<web>https://davdroid.bitfire.at/</web>
<source>https://davdroid.bitfire.at/source/</source>
<tracker>https://davdroid.bitfire.at/forums/</tracker>
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
<donate>https://davdroid.bitfire.at/donate/</donate>
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
<flattr>2100160</flattr>
<marketversion>1.3.1-ose</marketversion>
<marketvercode>116</marketvercode>
<package>
<version>1.3.1-ose</version>
<versioncode>116</versioncode>
<apkname>at.bitfire.davdroid_116.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-21</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.3-ose</version>
<versioncode>114</versioncode>
<apkname>at.bitfire.davdroid_114.apk</apkname>
<srcname>at.bitfire.davdroid_114_src.tar.gz</srcname>
<hash type="sha256">aaf956539aad7400269997bf1a6689f191b592e70146ffe5484312f9375df9d9
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3295326</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-05</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.2.3-ose</version>
<versioncode>112</versioncode>
<apkname>at.bitfire.davdroid_112.apk</apkname>
<srcname>at.bitfire.davdroid_112_src.tar.gz</srcname>
<hash type="sha256">045480c571f4dfabb23a5efb803b594c432c20ab33b6eb575b4476a8df22bb05
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3172611</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-12</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
</permissions>
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
</package>
</application>
<application id="com.murrayc.galaxyzoo.app">
<id>com.murrayc.galaxyzoo.app</id>
<added>2014-11-23</added>
<lastupdated>2016-09-02</lastupdated>
<name>Galaxy Zoo</name>
<summary>Help to classify galaxies</summary>
<icon>com.murrayc.galaxyzoo.app.64.png</icon>
<desc>
<p>Classify
<a href="http://www.galaxyzoo.org/">Galaxy Zoo</a>
subjects. Official approved by the<a href="https://www.zooniverse.org/">Zooniverse
project</a>.
</p>
<p>Asks you questions about a picture of a galaxy, with each question depending on the
previous question. This "Citizen Science" helps astronomers to analyze the huge
amount of images of galaxies provided, for instance, by the Hubble Space Telescope.
</p>
</desc>
<license>GPLv3</license>
<categories>Science &amp; Education</categories>
<category>Science &amp; Education</category>
<web />
<source>https://github.com/murraycu/android-galaxyzoo</source>
<tracker>https://github.com/murraycu/android-galaxyzoo/issues</tracker>
<marketversion>1.64</marketversion>
<marketvercode>64</marketvercode>
<package>
<version>1.64</version>
<versioncode>64</versioncode>
<apkname>com.murrayc.galaxyzoo.app_64.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_64_src.tar.gz</srcname>
<hash type="sha256">6f10487c8ef84078232aafe115228c6252ef982a228666454005b8d62c27fe42
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4095584</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-02</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.61</version>
<versioncode>61</versioncode>
<apkname>com.murrayc.galaxyzoo.app_61.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_61_src.tar.gz</srcname>
<hash type="sha256">c35bc15885d57a2ddfcb2c702c9147b63b63a9765a6eeacf98d6a1534f6f8ff2
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4588315</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-26</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.59</version>
<versioncode>59</versioncode>
<apkname>com.murrayc.galaxyzoo.app_59.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_59_src.tar.gz</srcname>
<hash type="sha256">719d1440dd2f16ad29b45edba5b43262f22ef68cee0c6b478e40fe8eb0e53227
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4116175</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-25</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
</application>
</fdroid>