diff --git a/app/src/main/java/org/fdroid/fdroid/AndroidXMLDecompress.java b/app/src/main/java/org/fdroid/fdroid/AndroidXMLDecompress.java deleted file mode 100644 index 35fcc6a4b..000000000 --- a/app/src/main/java/org/fdroid/fdroid/AndroidXMLDecompress.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2016 Hans-Christoph Steiner - * - * 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 apk-parser - * @see Android Internals: Binary XML - * @see a binary XML parser - */ -public class AndroidXMLDecompress { - private static final int START_TAG = 0x00100102; - - /** - * Just get the XML attributes from the {@code } element. - * - * @return A key value map of the attributes, with values as {@link Object}s - */ - public static Map 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 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 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; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java new file mode 100644 index 000000000..298631413 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.installer; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; + +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.Hasher; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.SanitizedFile; + +import java.io.File; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +public class ApkVerifier { + + Context context; + Uri localApkUri; + Apk apk; + PackageManager pm; + + ApkVerifier(Context context, Uri localApkUri, Apk apk) { + this.context = context; + this.localApkUri = localApkUri; + this.apk = apk; + this.pm = context.getPackageManager(); + } + + public void basicVerify() throws ApkVerificationException { + PackageInfo localApkInfo = pm.getPackageArchiveInfo( + localApkUri.getPath(), PackageManager.GET_PERMISSIONS); + if (localApkInfo == null) { + throw new ApkVerificationException("parsing apk failed!"); + } + + // check if the apk has the expected packageName + if (localApkInfo.packageName == null || !apk.packageName.equals(localApkInfo.packageName)) { + throw new ApkVerificationException("apk has unexpected packageName!"); + } + + if (localApkInfo.versionCode < 0) { + throw new ApkVerificationException("apk has no valid versionCode!"); + } + } + + 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, apk.hash, apk.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); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index b3bcf6218..7f48e7ca3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -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(), diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java index adb61d16d..b9902bd58 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -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; @@ -47,25 +46,17 @@ public class ExtensionInstaller extends Installer { @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(), diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index c0e2fab35..d80c4de84 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -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.basicVerify(); + 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() { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index 660508358..fc4ae1012 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -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 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); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 66bf38e88..ec8039880 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -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);