Move Apk verification and file sanitizing into own class
* use getPackageArchiveInfo of AOSP instead of AndroidXMLDecompress * verify in InstallManagerService instead of Installer subclasses
This commit is contained in:
parent
4c7217d796
commit
24ed40bd34
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
139
app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
Normal file
139
app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* 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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,7 +23,6 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.Utils;
|
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()));
|
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);
|
Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
|
||||||
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
|
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
|
||||||
installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
|
installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
|
||||||
installIntent.setData(sanitizedUri);
|
installIntent.setData(localApkUri);
|
||||||
|
|
||||||
PendingIntent installPendingIntent = PendingIntent.getActivity(
|
PendingIntent installPendingIntent = PendingIntent.getActivity(
|
||||||
context.getApplicationContext(),
|
context.getApplicationContext(),
|
||||||
|
@ -23,7 +23,6 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
||||||
@ -47,25 +46,17 @@ public class ExtensionInstaller extends Installer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void installPackage(Uri localApkUri, Uri downloadUri, String packageName) {
|
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
|
// 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);
|
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,
|
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED,
|
||||||
"APK signature of extension not correct!");
|
"APK signature of extension not correct!");
|
||||||
}
|
}
|
||||||
Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class);
|
Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class);
|
||||||
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
|
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
|
||||||
installIntent.setData(sanitizedUri);
|
installIntent.setData(localApkUri);
|
||||||
|
|
||||||
PendingIntent installPendingIntent = PendingIntent.getActivity(
|
PendingIntent installPendingIntent = PendingIntent.getActivity(
|
||||||
context.getApplicationContext(),
|
context.getApplicationContext(),
|
||||||
|
@ -15,6 +15,7 @@ import android.support.v4.app.NotificationCompat;
|
|||||||
import android.support.v4.app.TaskStackBuilder;
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.AppDetails;
|
import org.fdroid.fdroid.AppDetails;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
@ -186,7 +187,7 @@ public class InstallManagerService extends Service {
|
|||||||
private static boolean apkIsCached(File apkFile, Apk apkToCheck) {
|
private static boolean apkIsCached(File apkFile, Apk apkToCheck) {
|
||||||
try {
|
try {
|
||||||
return apkFile.length() == apkToCheck.size &&
|
return apkFile.length() == apkToCheck.size &&
|
||||||
Installer.verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType);
|
ApkVerifier.verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType);
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
@ -225,7 +226,6 @@ public class InstallManagerService extends Service {
|
|||||||
BroadcastReceiver completeReceiver = new BroadcastReceiver() {
|
BroadcastReceiver completeReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
// elsewhere called urlString
|
|
||||||
Uri downloadUri = intent.getData();
|
Uri downloadUri = intent.getData();
|
||||||
String urlString = downloadUri.toString();
|
String urlString = downloadUri.toString();
|
||||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||||
@ -237,7 +237,22 @@ public class InstallManagerService extends Service {
|
|||||||
registerInstallerReceivers(downloadUri);
|
registerInstallerReceivers(downloadUri);
|
||||||
|
|
||||||
Apk apk = ACTIVE_APKS.get(urlString);
|
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() {
|
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
||||||
|
@ -28,22 +28,13 @@ import android.os.PatternMatcher;
|
|||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
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.Apk;
|
||||||
import org.fdroid.fdroid.data.ApkProvider;
|
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.AppDiff;
|
||||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
|
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
|
||||||
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
|
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
|
||||||
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
|
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.
|
* Handles the actual install process. Subclasses implement the details.
|
||||||
*/
|
*/
|
||||||
@ -92,73 +83,6 @@ public abstract class Installer {
|
|||||||
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
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.
|
* Returns permission screen for given apk.
|
||||||
*
|
*
|
||||||
@ -227,18 +151,6 @@ public abstract class Installer {
|
|||||||
return intent;
|
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,
|
void sendBroadcastInstall(Uri downloadUri, String action,
|
||||||
PendingIntent pendingIntent) {
|
PendingIntent pendingIntent) {
|
||||||
sendBroadcastInstall(downloadUri, action, pendingIntent, null);
|
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) {
|
protected void installPackage(final Uri localApkUri, final Uri downloadUri, String packageName) {
|
||||||
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
|
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() {
|
ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
|
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
|
||||||
@ -335,7 +325,7 @@ public class PrivilegedInstaller extends Installer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
privService.installPackage(sanitizedUri, ACTION_INSTALL_REPLACE_EXISTING,
|
privService.installPackage(localApkUri, ACTION_INSTALL_REPLACE_EXISTING,
|
||||||
null, callback);
|
null, callback);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "RemoteException", e);
|
Log.e(TAG, "RemoteException", e);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user