Merge branch 'check-perms-after-install' into 'master'
Check permissions for unattended installer This PR introduces the class ``ApkVerifier`` which checks the permissions of the downloaded apk file against the expected permissions from the F-Droid listing (``Apk`` class). * I removed ``AndroidXMLDecompress`` because everything which it has been used for can also be done with ``PackageManager.getPackageArchiveInfo()``, to the best of my knowledge. I even asked in at a similar project why ``PackageManager.getPackageArchiveInfo()``may not be enough: https://github.com/jaredrummler/APKParser/issues/3 It turns out in our case it should do everything we need. * The code responsible for sanitizing the local apk file and making it world readable has also been moved into ``ApkVerifier`` for now. This can change in a later PR when I introduce the FileProvider for downloaded apks. We still need to check the target sdk version (see TODO in ``ApkVerifier``). This depends on https://gitlab.com/fdroid/fdroidclient/merge_requests/323 See merge request !322
This commit is contained in:
		
						commit
						7e27edc9b5
					
				| @ -1,172 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (C) 2016 Hans-Christoph Steiner <hans@eds.org> | ||||
|  * | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  Copyright (c) 2016, Liu Dong | ||||
|  All rights reserved. | ||||
| 
 | ||||
|  Redistribution and use in source and binary forms, with or without | ||||
|  modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
|  * Redistributions of source code must retain the above copyright notice, this | ||||
|  list of conditions and the following disclaimer. | ||||
| 
 | ||||
|  * Redistributions in binary form must reproduce the above copyright notice, | ||||
|  this list of conditions and the following disclaimer in the documentation | ||||
|  and/or other materials provided with the distribution. | ||||
| 
 | ||||
|  * Neither the name of apk-parser nor the names of its | ||||
|  contributors may be used to endorse or promote products derived from | ||||
|  this software without specific prior written permission. | ||||
| 
 | ||||
|  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| package org.fdroid.fdroid; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.zip.ZipEntry; | ||||
| import java.util.zip.ZipFile; | ||||
| 
 | ||||
| /** | ||||
|  * Parse the 'compressed' binary form of Android XML docs such as for | ||||
|  * {@code AndroidManifest.xml} in APK files.  This is a very truncated | ||||
|  * version of apk-parser since currently, we only need the header from | ||||
|  * the binary XML AndroidManifest.xml. apk-parser provides full APK | ||||
|  * parsing, which is a lot more than what is needed. | ||||
|  * | ||||
|  * @see <a href="https://github.com/caoqianli/apk-parser">apk-parser</a> | ||||
|  * @see <a href="https://justanapplication.wordpress.com/category/android/android-binary-xml">Android Internals: Binary XML</a> | ||||
|  * @see <a href="https://stackoverflow.com/a/4761689">a binary XML parser</a> | ||||
|  */ | ||||
| public class AndroidXMLDecompress { | ||||
|     private static final int START_TAG = 0x00100102; | ||||
| 
 | ||||
|     /** | ||||
|      * Just get the XML attributes from the {@code <manifest>} element. | ||||
|      * | ||||
|      * @return A key value map of the attributes, with values as {@link Object}s | ||||
|      */ | ||||
|     public static Map<String, Object> getManifestHeaderAttributes(String filename) throws IOException { | ||||
|         byte[] binaryXml = getManifestFromFilename(filename); | ||||
|         int numbStrings = littleEndianWord(binaryXml, 4 * 4); | ||||
|         int stringIndexTableOffset = 0x24; | ||||
|         int stringTableOffset = stringIndexTableOffset + numbStrings * 4; | ||||
|         int xmlTagOffset = littleEndianWord(binaryXml, 3 * 4); | ||||
|         for (int i = xmlTagOffset; i < binaryXml.length - 4; i += 4) { | ||||
|             if (littleEndianWord(binaryXml, i) == START_TAG) { | ||||
|                 xmlTagOffset = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         int offset = xmlTagOffset; | ||||
| 
 | ||||
|         // we only need the first <manifest> start tag | ||||
|         if (offset < binaryXml.length) { | ||||
|             int tag0 = littleEndianWord(binaryXml, offset); | ||||
| 
 | ||||
|             if (tag0 == START_TAG) { | ||||
|                 int numbAttrs = littleEndianWord(binaryXml, offset + 7 * 4); | ||||
|                 offset += 9 * 4; | ||||
| 
 | ||||
|                 HashMap<String, Object> attributes = new HashMap<>(3); | ||||
|                 for (int i = 0; i < numbAttrs; i++) { | ||||
|                     int attributeNameStringIndex = littleEndianWord(binaryXml, offset + 1 * 4); | ||||
|                     int attributeValueStringIndex = littleEndianWord(binaryXml, offset + 2 * 4); | ||||
|                     int attributeResourceId = littleEndianWord(binaryXml, offset + 4 * 4); | ||||
|                     offset += 5 * 4; | ||||
| 
 | ||||
|                     String attributeName = getString(binaryXml, stringIndexTableOffset, stringTableOffset, attributeNameStringIndex); | ||||
|                     Object attributeValue; | ||||
|                     if (attributeValueStringIndex != -1) { | ||||
|                         attributeValue = getString(binaryXml, stringIndexTableOffset, stringTableOffset, attributeValueStringIndex); | ||||
|                     } else { | ||||
|                         attributeValue = attributeResourceId; | ||||
|                     } | ||||
|                     attributes.put(attributeName, attributeValue); | ||||
|                 } | ||||
|                 return attributes; | ||||
|             } | ||||
|         } | ||||
|         return new HashMap<>(0); | ||||
|     } | ||||
| 
 | ||||
|     private static byte[] getManifestFromFilename(String filename) throws IOException { | ||||
|         InputStream is = null; | ||||
|         ZipFile zip = null; | ||||
|         int size = 0; | ||||
| 
 | ||||
|         if (filename.endsWith(".apk")) { | ||||
|             zip = new ZipFile(filename); | ||||
|             ZipEntry ze = zip.getEntry("AndroidManifest.xml"); | ||||
|             is = zip.getInputStream(ze); | ||||
|             size = (int) ze.getSize(); | ||||
|         } else { | ||||
|             throw new RuntimeException("This only works on APK files!"); | ||||
|         } | ||||
|         byte[] buf = new byte[size]; | ||||
|         is.read(buf); | ||||
| 
 | ||||
|         is.close(); | ||||
|         zip.close(); | ||||
|         return buf; | ||||
|     } | ||||
| 
 | ||||
|     private static String getString(byte[] bytes, int stringIndexTableOffset, int stringTableOffset, int stringIndex) { | ||||
|         if (stringIndex < 0) { | ||||
|             return null; | ||||
|         } | ||||
|         int stringOffset = stringTableOffset + littleEndianWord(bytes, stringIndexTableOffset + stringIndex * 4); | ||||
|         return getStringAt(bytes, stringOffset); | ||||
|     } | ||||
| 
 | ||||
|     private static String getStringAt(byte[] bytes, int stringOffset) { | ||||
|         int length = bytes[stringOffset + 1] << 8 & 0xff00 | bytes[stringOffset] & 0xff; | ||||
|         byte[] chars = new byte[length]; | ||||
|         for (int i = 0; i < length; i++) { | ||||
|             chars[i] = bytes[stringOffset + 2 + i * 2]; | ||||
|         } | ||||
|         return new String(chars); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the little endian 32-bit word from the byte array at offset | ||||
|      */ | ||||
|     private static int littleEndianWord(byte[] bytes, int offset) { | ||||
|         return bytes[offset + 3] | ||||
|                 << 24 & 0xff000000 | ||||
|                 | bytes[offset + 2] | ||||
|                 << 16 & 0xff0000 | ||||
|                 | bytes[offset + 1] | ||||
|                 << 8 & 0xff00 | ||||
|                 | bytes[offset] & 0xFF; | ||||
|     } | ||||
| } | ||||
| @ -78,7 +78,6 @@ import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.assist.ImageScaleType; | ||||
| 
 | ||||
| import org.fdroid.fdroid.Utils.CommaSeparatedList; | ||||
| import org.fdroid.fdroid.data.Apk; | ||||
| import org.fdroid.fdroid.data.ApkProvider; | ||||
| import org.fdroid.fdroid.data.App; | ||||
| @ -92,7 +91,7 @@ import org.fdroid.fdroid.installer.InstallerService; | ||||
| import org.fdroid.fdroid.net.Downloader; | ||||
| import org.fdroid.fdroid.net.DownloaderService; | ||||
| 
 | ||||
| import java.util.Iterator; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class AppDetails extends AppCompatActivity { | ||||
| @ -1375,14 +1374,12 @@ public class AppDetails extends AppCompatActivity { | ||||
|         private void buildPermissionInfo() { | ||||
|             final TextView permissionListView = (TextView) llViewMorePermissions.findViewById(R.id.permissions_list); | ||||
| 
 | ||||
|             CommaSeparatedList permsList = appDetails.getApks().getItem(0).permissions; | ||||
|             ArrayList<String> permsList = appDetails.getApks().getItem(0).getFullPermissionList(); | ||||
|             if (permsList == null) { | ||||
|                 permissionListView.setText(R.string.no_permissions); | ||||
|             } else { | ||||
|                 Iterator<String> permissions = permsList.iterator(); | ||||
|                 StringBuilder sb = new StringBuilder(); | ||||
|                 while (permissions.hasNext()) { | ||||
|                     final String permissionName = permissions.next(); | ||||
|                 for (String permissionName : permsList) { | ||||
|                     try { | ||||
|                         final Permission permission = new Permission(getActivity(), permissionName); | ||||
|                         // TODO: Make this list RTL friendly | ||||
|  | ||||
| @ -4,7 +4,7 @@ import android.content.Context; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.content.pm.PermissionInfo; | ||||
| 
 | ||||
| class Permission { | ||||
| public class Permission { | ||||
| 
 | ||||
|     private final PackageManager packageManager; | ||||
|     private final PermissionInfo permissionInfo; | ||||
| @ -13,17 +13,7 @@ class Permission { | ||||
|             throws PackageManager.NameNotFoundException { | ||||
|         this.packageManager = context.getPackageManager(); | ||||
|         this.permissionInfo = this.packageManager.getPermissionInfo( | ||||
|                 fdroidToAndroid(permission), PackageManager.GET_META_DATA); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * It appears that all of the 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." | ||||
|      */ | ||||
|     private static String fdroidToAndroid(String permission) { | ||||
|         return "android.permission." + permission; | ||||
|                 permission, PackageManager.GET_META_DATA); | ||||
|     } | ||||
| 
 | ||||
|     public CharSequence getName() { | ||||
|  | ||||
| @ -8,7 +8,9 @@ import android.os.Parcelable; | ||||
| 
 | ||||
| import org.fdroid.fdroid.Utils; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Date; | ||||
| import java.util.HashSet; | ||||
| 
 | ||||
| public class Apk extends ValueObject implements Comparable<Apk> { | ||||
| 
 | ||||
| @ -141,6 +143,43 @@ public class Apk extends ValueObject implements Comparable<Apk> { | ||||
|         return repoAddress + "/" + apkName.replace(" ", "%20"); | ||||
|     } | ||||
| 
 | ||||
|     public ArrayList<String> getFullPermissionList() { | ||||
|         if (this.permissions == null) { | ||||
|             return new ArrayList<>(); | ||||
|         } | ||||
| 
 | ||||
|         ArrayList<String> permissionsFull = new ArrayList<>(); | ||||
|         for (String perm : this.permissions) { | ||||
|             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 https://gitlab.com/fdroid/fdroidserver/blob/master/fdroidserver/update.py#L535# | ||||
|      */ | ||||
|     public static String fdroidToAndroidPermission(String permission) { | ||||
|         if (!permission.contains(".")) { | ||||
|             return "android.permission." + permission; | ||||
|         } | ||||
| 
 | ||||
|         return permission; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return packageName + " (version " + versionCode + ")"; | ||||
|  | ||||
							
								
								
									
										174
									
								
								app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| /* | ||||
|  * 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.content.Context; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import org.apache.commons.io.FileUtils; | ||||
| import org.fdroid.fdroid.Hasher; | ||||
| import org.fdroid.fdroid.Utils; | ||||
| import org.fdroid.fdroid.data.Apk; | ||||
| import org.fdroid.fdroid.data.SanitizedFile; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashSet; | ||||
| 
 | ||||
| /** | ||||
|  * This ApkVerifier verifies that the downloaded apk corresponds to the Apk information | ||||
|  * displayed to the user. This is especially important in case an unattended installer | ||||
|  * has been used which displays permissions before download. | ||||
|  */ | ||||
| public class ApkVerifier { | ||||
| 
 | ||||
|     private static final String TAG = "ApkVerifier"; | ||||
| 
 | ||||
|     private final Context context; | ||||
|     private final Uri localApkUri; | ||||
|     private final Apk expectedApk; | ||||
|     private final PackageManager pm; | ||||
| 
 | ||||
|     ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) { | ||||
|         this.context = context; | ||||
|         this.localApkUri = localApkUri; | ||||
|         this.expectedApk = expectedApk; | ||||
|         this.pm = context.getPackageManager(); | ||||
|     } | ||||
| 
 | ||||
|     public void verifyApk() throws ApkVerificationException { | ||||
|         // parse downloaded apk file locally | ||||
|         PackageInfo localApkInfo = pm.getPackageArchiveInfo( | ||||
|                 localApkUri.getPath(), PackageManager.GET_PERMISSIONS); | ||||
|         if (localApkInfo == null) { | ||||
|             throw new ApkVerificationException("parsing apk file failed!"); | ||||
|         } | ||||
| 
 | ||||
|         // check if the apk has the expected packageName | ||||
|         if (!TextUtils.equals(localApkInfo.packageName, expectedApk.packageName)) { | ||||
|             throw new ApkVerificationException("apk has unexpected packageName!"); | ||||
|         } | ||||
| 
 | ||||
|         if (localApkInfo.versionCode < 0) { | ||||
|             throw new ApkVerificationException("apk has no valid versionCode!"); | ||||
|         } | ||||
| 
 | ||||
|         // verify permissions, important for unattended installer | ||||
|         HashSet<String> localPermissions = getLocalPermissionsSet(localApkInfo); | ||||
|         HashSet<String> expectedPermissions = expectedApk.getFullPermissionsSet(); | ||||
|         Utils.debugLog(TAG, "localPermissions: " + localPermissions); | ||||
|         Utils.debugLog(TAG, "expectedPermissions: " + expectedPermissions); | ||||
|         if (!localPermissions.equals(expectedPermissions)) { | ||||
|             throw new ApkVerificationException("permissions of apk not equals expected permissions!"); | ||||
|         } | ||||
| 
 | ||||
|         int localTargetSdkVersion = localApkInfo.applicationInfo.targetSdkVersion; | ||||
|         Utils.debugLog(TAG, "localTargetSdkVersion: " + localTargetSdkVersion); | ||||
|         // TODO: check target sdk | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private HashSet<String> getLocalPermissionsSet(PackageInfo localApkInfo) { | ||||
|         String[] localPermissions = localApkInfo.requestedPermissions; | ||||
|         if (localPermissions == null) { | ||||
|             return new HashSet<>(); | ||||
|         } | ||||
| 
 | ||||
|         return new HashSet<>(Arrays.asList(localApkInfo.requestedPermissions)); | ||||
|     } | ||||
| 
 | ||||
|     public Uri getSafeUri() throws ApkVerificationException { | ||||
|         File apkFile = new File(localApkUri.getPath()); | ||||
| 
 | ||||
|         SanitizedFile sanitizedApkFile = null; | ||||
|         try { | ||||
| 
 | ||||
|             /* Always copy the APK to the safe location inside of the protected area | ||||
|              * of the app to prevent attacks based on other apps swapping the file | ||||
|              * out during the install process. Most likely, apkFile was just downloaded, | ||||
|              * so it should still be in the RAM disk cache */ | ||||
|             sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", | ||||
|                     context.getFilesDir())); | ||||
|             FileUtils.copyFile(apkFile, sanitizedApkFile); | ||||
|             if (!verifyApkFile(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) { | ||||
|                 FileUtils.deleteQuietly(apkFile); | ||||
|                 throw new ApkVerificationException(apkFile + " failed to verify!"); | ||||
|             } | ||||
|             apkFile = null; // ensure this is not used now that its copied to apkToInstall | ||||
| 
 | ||||
|             // Need the apk to be world readable, so that the installer is able to read it. | ||||
|             // Note that saving it into external storage for the purpose of letting the installer | ||||
|             // have access is insecure, because apps with permission to write to the external | ||||
|             // storage can overwrite the app between F-Droid asking for it to be installed and | ||||
|             // the installer actually installing it. | ||||
|             sanitizedApkFile.setReadable(true, false); | ||||
| 
 | ||||
|         } catch (IOException | NoSuchAlgorithmException e) { | ||||
|             throw new ApkVerificationException(e); | ||||
|         } finally { | ||||
|             // 20 minutes the start of the install process, delete the file | ||||
|             final File apkToDelete = sanitizedApkFile; | ||||
|             new Thread() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); | ||||
|                     try { | ||||
|                         Thread.sleep(1200000); | ||||
|                     } catch (InterruptedException e) { | ||||
|                         e.printStackTrace(); | ||||
|                     } finally { | ||||
|                         FileUtils.deleteQuietly(apkToDelete); | ||||
|                     } | ||||
|                 } | ||||
|             }.start(); | ||||
|         } | ||||
| 
 | ||||
|         return Uri.fromFile(sanitizedApkFile); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks the APK file against the provided hash, returning whether it is a match. | ||||
|      */ | ||||
|     static boolean verifyApkFile(File apkFile, String hash, String hashType) | ||||
|             throws NoSuchAlgorithmException { | ||||
|         if (!apkFile.exists()) { | ||||
|             return false; | ||||
|         } | ||||
|         Hasher hasher = new Hasher(hashType, apkFile); | ||||
|         return hasher.match(hash); | ||||
|     } | ||||
| 
 | ||||
|     public static class ApkVerificationException extends Exception { | ||||
| 
 | ||||
|         public ApkVerificationException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
| 
 | ||||
|         public ApkVerificationException(Throwable cause) { | ||||
|             super(cause); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -23,7 +23,6 @@ import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.fdroid.fdroid.Utils; | ||||
| 
 | ||||
| @ -50,20 +49,10 @@ public class DefaultInstaller extends Installer { | ||||
| 
 | ||||
|         Utils.debugLog(TAG, "DefaultInstaller uri: " + localApkUri + " file: " + new File(localApkUri.getPath())); | ||||
| 
 | ||||
|         Uri sanitizedUri; | ||||
|         try { | ||||
|             sanitizedUri = Installer.prepareApkFile(context, localApkUri, packageName); | ||||
|         } catch (Installer.InstallFailedException e) { | ||||
|             Log.e(TAG, "prepareApkFile failed", e); | ||||
|             sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, | ||||
|                     e.getMessage()); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Intent installIntent = new Intent(context, DefaultInstallerActivity.class); | ||||
|         installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); | ||||
|         installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri); | ||||
|         installIntent.setData(sanitizedUri); | ||||
|         installIntent.setData(localApkUri); | ||||
| 
 | ||||
|         PendingIntent installPendingIntent = PendingIntent.getActivity( | ||||
|                 context.getApplicationContext(), | ||||
|  | ||||
| @ -23,7 +23,6 @@ import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.fdroid.fdroid.BuildConfig; | ||||
| import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; | ||||
| @ -39,33 +38,23 @@ import java.io.File; | ||||
|  */ | ||||
| public class ExtensionInstaller extends Installer { | ||||
| 
 | ||||
|     private static final String TAG = "ExtensionInstaller"; | ||||
| 
 | ||||
|     ExtensionInstaller(Context context) { | ||||
|         super(context); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void installPackage(Uri localApkUri, Uri downloadUri, String packageName) { | ||||
|         Uri sanitizedUri; | ||||
|         try { | ||||
|             sanitizedUri = Installer.prepareApkFile(context, localApkUri, packageName); | ||||
|         } catch (InstallFailedException e) { | ||||
|             Log.e(TAG, "prepareApkFile failed", e); | ||||
|             sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage()); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // extension must be signed with the same public key as main F-Droid | ||||
|         // NOTE: Disabled for debug builds to be able to use official extension from repo | ||||
|         // NOTE: Disabled for debug builds to be able to test official extension from repo | ||||
|         ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context); | ||||
|         if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { | ||||
|         if (!BuildConfig.DEBUG && | ||||
|                 !signatureVerifier.hasFDroidSignature(new File(localApkUri.getPath()))) { | ||||
|             sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, | ||||
|                     "APK signature of extension not correct!"); | ||||
|         } | ||||
|         Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class); | ||||
|         installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); | ||||
|         installIntent.setData(sanitizedUri); | ||||
|         installIntent.setData(localApkUri); | ||||
| 
 | ||||
|         PendingIntent installPendingIntent = PendingIntent.getActivity( | ||||
|                 context.getApplicationContext(), | ||||
|  | ||||
| @ -15,6 +15,7 @@ import android.support.v4.app.NotificationCompat; | ||||
| import android.support.v4.app.TaskStackBuilder; | ||||
| import android.support.v4.content.LocalBroadcastManager; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.fdroid.fdroid.AppDetails; | ||||
| import org.fdroid.fdroid.R; | ||||
| @ -186,7 +187,7 @@ public class InstallManagerService extends Service { | ||||
|     private static boolean apkIsCached(File apkFile, Apk apkToCheck) { | ||||
|         try { | ||||
|             return apkFile.length() == apkToCheck.size && | ||||
|                     Installer.verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType); | ||||
|                     ApkVerifier.verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType); | ||||
|         } catch (NoSuchAlgorithmException e) { | ||||
|             e.printStackTrace(); | ||||
|             return false; | ||||
| @ -225,7 +226,6 @@ public class InstallManagerService extends Service { | ||||
|         BroadcastReceiver completeReceiver = new BroadcastReceiver() { | ||||
|             @Override | ||||
|             public void onReceive(Context context, Intent intent) { | ||||
|                 // elsewhere called urlString | ||||
|                 Uri downloadUri = intent.getData(); | ||||
|                 String urlString = downloadUri.toString(); | ||||
|                 File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); | ||||
| @ -237,7 +237,22 @@ public class InstallManagerService extends Service { | ||||
|                 registerInstallerReceivers(downloadUri); | ||||
| 
 | ||||
|                 Apk apk = ACTIVE_APKS.get(urlString); | ||||
|                 InstallerService.install(context, localApkUri, downloadUri, apk.packageName); | ||||
| 
 | ||||
|                 Uri sanitizedUri; | ||||
|                 try { | ||||
|                     ApkVerifier apkVerifier = new ApkVerifier(context, localApkUri, apk); | ||||
|                     apkVerifier.verifyApk(); | ||||
|                     sanitizedUri = apkVerifier.getSafeUri(); | ||||
|                 } catch (ApkVerifier.ApkVerificationException e) { | ||||
|                     Log.e(TAG, "ApkVerifier failed", e); | ||||
|                     String title = String.format( | ||||
|                             getString(R.string.install_error_notify_title), | ||||
|                             apk.packageName); | ||||
|                     notifyError(urlString, title, e.getMessage()); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 InstallerService.install(context, sanitizedUri, downloadUri, apk.packageName); | ||||
|             } | ||||
|         }; | ||||
|         BroadcastReceiver interruptedReceiver = new BroadcastReceiver() { | ||||
|  | ||||
| @ -28,22 +28,13 @@ import android.os.PatternMatcher; | ||||
| import android.support.v4.content.LocalBroadcastManager; | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import org.apache.commons.io.FileUtils; | ||||
| import org.fdroid.fdroid.AndroidXMLDecompress; | ||||
| import org.fdroid.fdroid.Hasher; | ||||
| import org.fdroid.fdroid.data.Apk; | ||||
| import org.fdroid.fdroid.data.ApkProvider; | ||||
| import org.fdroid.fdroid.data.SanitizedFile; | ||||
| import org.fdroid.fdroid.privileged.views.AppDiff; | ||||
| import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; | ||||
| import org.fdroid.fdroid.privileged.views.InstallConfirmActivity; | ||||
| import org.fdroid.fdroid.privileged.views.UninstallDialogActivity; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * Handles the actual install process.  Subclasses implement the details. | ||||
|  */ | ||||
| @ -92,73 +83,6 @@ public abstract class Installer { | ||||
|         localBroadcastManager = LocalBroadcastManager.getInstance(context); | ||||
|     } | ||||
| 
 | ||||
|     static Uri prepareApkFile(Context context, Uri uri, String packageName) | ||||
|             throws InstallFailedException { | ||||
| 
 | ||||
|         File apkFile = new File(uri.getPath()); | ||||
| 
 | ||||
|         SanitizedFile sanitizedApkFile = null; | ||||
|         try { | ||||
|             Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath()); | ||||
| 
 | ||||
|             /* This isn't really needed, but might as well since we have the data already */ | ||||
|             if (attributes.containsKey("packageName") && !TextUtils.equals(packageName, (String) attributes.get("packageName"))) { | ||||
|                 throw new InstallFailedException(uri + " has packageName that clashes with " + packageName); | ||||
|             } | ||||
| 
 | ||||
|             if (!attributes.containsKey("versionCode")) { | ||||
|                 throw new InstallFailedException(uri + " is missing versionCode!"); | ||||
|             } | ||||
|             int versionCode = (Integer) attributes.get("versionCode"); | ||||
|             Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{ | ||||
|                     ApkProvider.DataColumns.HASH, | ||||
|                     ApkProvider.DataColumns.HASH_TYPE, | ||||
|             }); | ||||
|             /* Always copy the APK to the safe location inside of the protected area | ||||
|              * of the app to prevent attacks based on other apps swapping the file | ||||
|              * out during the install process. Most likely, apkFile was just downloaded, | ||||
|              * so it should still be in the RAM disk cache */ | ||||
|             sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", | ||||
|                     context.getFilesDir())); | ||||
|             FileUtils.copyFile(apkFile, sanitizedApkFile); | ||||
|             if (!verifyApkFile(sanitizedApkFile, apk.hash, apk.hashType)) { | ||||
|                 FileUtils.deleteQuietly(apkFile); | ||||
|                 throw new InstallFailedException(apkFile + " failed to verify!"); | ||||
|             } | ||||
|             apkFile = null; // ensure this is not used now that its copied to apkToInstall | ||||
| 
 | ||||
|             // Need the apk to be world readable, so that the installer is able to read it. | ||||
|             // Note that saving it into external storage for the purpose of letting the installer | ||||
|             // have access is insecure, because apps with permission to write to the external | ||||
|             // storage can overwrite the app between F-Droid asking for it to be installed and | ||||
|             // the installer actually installing it. | ||||
|             sanitizedApkFile.setReadable(true, false); | ||||
| 
 | ||||
|         } catch (NumberFormatException | IOException | NoSuchAlgorithmException e) { | ||||
|             throw new InstallFailedException(e); | ||||
|         } catch (ClassCastException e) { | ||||
|             throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!"); | ||||
|         } finally { | ||||
|             // 20 minutes the start of the install process, delete the file | ||||
|             final File apkToDelete = sanitizedApkFile; | ||||
|             new Thread() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); | ||||
|                     try { | ||||
|                         Thread.sleep(1200000); | ||||
|                     } catch (InterruptedException e) { | ||||
|                         e.printStackTrace(); | ||||
|                     } finally { | ||||
|                         FileUtils.deleteQuietly(apkToDelete); | ||||
|                     } | ||||
|                 } | ||||
|             }.start(); | ||||
|         } | ||||
| 
 | ||||
|         return Uri.fromFile(sanitizedApkFile); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns permission screen for given apk. | ||||
|      * | ||||
| @ -227,18 +151,6 @@ public abstract class Installer { | ||||
|         return intent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks the APK file against the provided hash, returning whether it is a match. | ||||
|      */ | ||||
|     static boolean verifyApkFile(File apkFile, String hash, String hashType) | ||||
|             throws NoSuchAlgorithmException { | ||||
|         if (!apkFile.exists()) { | ||||
|             return false; | ||||
|         } | ||||
|         Hasher hasher = new Hasher(hashType, apkFile); | ||||
|         return hasher.match(hash); | ||||
|     } | ||||
| 
 | ||||
|     void sendBroadcastInstall(Uri downloadUri, String action, | ||||
|                                      PendingIntent pendingIntent) { | ||||
|         sendBroadcastInstall(downloadUri, action, pendingIntent, null); | ||||
|  | ||||
| @ -300,16 +300,6 @@ public class PrivilegedInstaller extends Installer { | ||||
|     protected void installPackage(final Uri localApkUri, final Uri downloadUri, String packageName) { | ||||
|         sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED); | ||||
| 
 | ||||
|         final Uri sanitizedUri; | ||||
|         try { | ||||
|             sanitizedUri = Installer.prepareApkFile(context, localApkUri, packageName); | ||||
|         } catch (Installer.InstallFailedException e) { | ||||
|             Log.e(TAG, "prepareApkFile failed", e); | ||||
|             sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, | ||||
|                     e.getMessage()); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         ServiceConnection mServiceConnection = new ServiceConnection() { | ||||
|             public void onServiceConnected(ComponentName name, IBinder service) { | ||||
|                 IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); | ||||
| @ -335,7 +325,7 @@ public class PrivilegedInstaller extends Installer { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     privService.installPackage(sanitizedUri, ACTION_INSTALL_REPLACE_EXISTING, | ||||
|                     privService.installPackage(localApkUri, ACTION_INSTALL_REPLACE_EXISTING, | ||||
|                             null, callback); | ||||
|                 } catch (RemoteException e) { | ||||
|                     Log.e(TAG, "RemoteException", e); | ||||
|  | ||||
| @ -18,17 +18,12 @@ | ||||
| 
 | ||||
| package org.fdroid.fdroid.privileged.views; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.content.pm.ApplicationInfo; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import org.fdroid.fdroid.data.Apk; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| 
 | ||||
| @TargetApi(Build.VERSION_CODES.M) | ||||
| public class AppDiff { | ||||
| 
 | ||||
|     private final PackageManager mPm; | ||||
| @ -45,17 +40,7 @@ public class AppDiff { | ||||
|         mPkgInfo = new PackageInfo(); | ||||
|         mPkgInfo.packageName = apk.packageName; | ||||
|         mPkgInfo.applicationInfo = new ApplicationInfo(); | ||||
| 
 | ||||
|         if (apk.permissions == null) { | ||||
|             mPkgInfo.requestedPermissions = null; | ||||
|         } else { | ||||
|             // TODO: duplicate code with Permission.fdroidToAndroid | ||||
|             ArrayList<String> permissionsFixed = new ArrayList<>(); | ||||
|             for (String perm : apk.permissions.toArrayList()) { | ||||
|                 permissionsFixed.add("android.permission." + perm); | ||||
|             } | ||||
|             mPkgInfo.requestedPermissions = permissionsFixed.toArray(new String[permissionsFixed.size()]); | ||||
|         } | ||||
|         mPkgInfo.requestedPermissions = apk.getFullPermissionsArray(); | ||||
| 
 | ||||
|         init(); | ||||
|     } | ||||
|  | ||||
| @ -1,51 +0,0 @@ | ||||
| package org.fdroid.fdroid; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.FilenameFilter; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| public class AndroidXMLDecompressTest { | ||||
| 
 | ||||
|     String[] testDirNames = { | ||||
|             System.getProperty("user.dir") + "/src/test/assets", | ||||
|             System.getProperty("user.dir") + "/build/outputs/apk", | ||||
|             System.getenv("HOME") + "/fdroid/repo", | ||||
|     }; | ||||
| 
 | ||||
|     FilenameFilter apkFilter = new FilenameFilter() { | ||||
|         @Override | ||||
|         public boolean accept(File dir, String filename) { | ||||
|             return filename.endsWith(".apk"); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     @Test | ||||
|     public void testParseVersionCode() throws IOException { | ||||
|         for (File f : getFilesToTest()) { | ||||
|             System.out.println("\n" + f); | ||||
|             Map<String, Object> map = AndroidXMLDecompress.getManifestHeaderAttributes(f.getAbsolutePath()); | ||||
|             for (String key : map.keySet()) { | ||||
|                 System.out.println(key + "=\"" + map.get(key) + "\""); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private List<File> getFilesToTest() { | ||||
|         ArrayList<File> apkFiles = new ArrayList<File>(5); | ||||
|         for (String dirName : testDirNames) { | ||||
|             System.out.println("looking in " + dirName); | ||||
|             File dir = new File(dirName); | ||||
|             File[] files = dir.listFiles(apkFilter); | ||||
|             if (files != null) { | ||||
|                 apkFiles.addAll(Arrays.asList(files)); | ||||
|             } | ||||
|         } | ||||
|         return apkFiles; | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Daniel Martí
						Daniel Martí