/*
* Copyright (C) 2016 Blue Jay Wireless
* Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 Daniel Martà <mvdan@mvdan.cc>
*
* 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.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
import org.fdroid.fdroid.privileged.IPrivilegedService;
import java.util.HashMap;
/**
* Installer that only works if the "F-Droid Privileged
* Extension" is installed as a privileged app.
* <p/>
* "F-Droid Privileged Extension" provides a service that exposes
* internal Android APIs for install/uninstall which are protected
* by INSTALL_PACKAGES, DELETE_PACKAGES permissions.
* Both permissions are protected by systemOrSignature (in newer versions:
* system|signature) and cannot be used directly by F-Droid.
* <p/>
* Instead, this installer binds to the service of
* "F-Droid Privileged Extension" and then executes the appropriate methods
* inside the privileged context of the privileged extension.
* <p/>
* This installer makes unattended installs/uninstalls possible.
* Thus no PendingIntents are returned.
* <p/>
* Sources for Android 4.4 change:
* https://groups.google.com/forum/#!msg/android-
* security-discuss/r7uL_OEMU5c/LijNHvxeV80J
* https://android.googlesource.com/platform
* /frameworks/base/+/ccbf84f44c9e6a5ed3c08673614826bb237afc54
*/
public class PrivilegedInstaller extends Installer {
private static final String TAG = "PrivilegedInstaller";
private static final String PRIVILEGED_EXTENSION_SERVICE_INTENT = "org.fdroid.fdroid.privileged.IPrivilegedService";
public static final String PRIVILEGED_EXTENSION_PACKAGE_NAME = "org.fdroid.fdroid.privileged";
public static final int IS_EXTENSION_INSTALLED_NO = 0;
public static final int IS_EXTENSION_INSTALLED_YES = 1;
public static final int IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM = 2;
// From AOSP source code
public static final int ACTION_INSTALL_REPLACE_EXISTING = 2;
/**
* Following return codes are copied from AOSP 5.1 source code
*/
public static final int INSTALL_SUCCEEDED = 1;
public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
public static final int INSTALL_FAILED_INVALID_APK = -2;
public static final int INSTALL_FAILED_INVALID_URI = -3;
public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
public static final int INSTALL_FAILED_DEXOPT = -11;
public static final int INSTALL_FAILED_OLDER_SDK = -12;
public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13;
public static final int INSTALL_FAILED_NEWER_SDK = -14;
public static final int INSTALL_FAILED_TEST_ONLY = -15;
public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16;
public static final int INSTALL_FAILED_MISSING_FEATURE = -17;
public static final int INSTALL_FAILED_CONTAINER_ERROR = -18;
public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19;
public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20;
public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21;
public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;
public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23;
public static final int INSTALL_FAILED_UID_CHANGED = -24;
public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;
public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
public static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
public static final int INSTALL_FAILED_USER_RESTRICTED = -111;
public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112;
public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113;
/**
* Internal return code for NativeLibraryHelper methods to indicate that the package
* being processed did not contain any native code. This is placed here only so that
* it can belong to the same value space as the other install failure codes.
*/
public static final int NO_NATIVE_LIBRARIES = -114;
public static final int INSTALL_FAILED_ABORTED = -115;
private static final HashMap<Integer, String> INSTALL_RETURN_CODES;
static {
// Descriptions extracted from the source code comments in AOSP
INSTALL_RETURN_CODES = new HashMap<>();
INSTALL_RETURN_CODES.put(INSTALL_SUCCEEDED,
"Success");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_ALREADY_EXISTS,
"Package is already installed.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_APK,
"The package archive file is invalid.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_URI,
"The URI passed in is invalid.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_INSUFFICIENT_STORAGE,
"The package manager service found that the device didn't have enough " +
"storage space to install the app.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_DUPLICATE_PACKAGE,
"A package is already installed with the same name.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_NO_SHARED_USER,
"The requested shared user does not exist.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"A previously installed package of the same name has a different signature than " +
"the new package (and the old package's data was not removed).");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"The new package is requested a shared user which is already installed on " +
"the device and does not have matching signature.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
"The new package uses a shared library that is not available.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_REPLACE_COULDNT_DELETE,
"Unknown"); // wrong comment in source
INSTALL_RETURN_CODES.put(INSTALL_FAILED_DEXOPT,
"The package failed while optimizing and validating its dex files, either " +
"because there was not enough storage or the validation failed.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_OLDER_SDK,
"The new package failed because the current SDK version is older than that " +
"required by the package.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_CONFLICTING_PROVIDER,
"The new package failed because it contains a content provider with the same " +
"authority as a provider already installed in the system.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_NEWER_SDK,
"The new package failed because the current SDK version is newer than that " +
"required by the package.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_TEST_ONLY,
"The new package failed because it has specified that it is a test-only package " +
"and the caller has not supplied the {@link #INSTALL_ALLOW_TEST} flag.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_CPU_ABI_INCOMPATIBLE,
"The package being installed contains native code, but none that is compatible " +
"with the device's CPU_ABI.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_FEATURE,
"The new package uses a feature that is not available.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_CONTAINER_ERROR,
"A secure container mount point couldn't be accessed on external media.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
"The new package couldn't be installed in the specified install location.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_MEDIA_UNAVAILABLE,
"The new package couldn't be installed in the specified install location " +
"because the media is not available.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_TIMEOUT,
"The new package couldn't be installed because the verification timed out.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_FAILURE,
"The new package couldn't be installed because the verification did not succeed.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_PACKAGE_CHANGED,
"The package changed from what the calling program expected.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_UID_CHANGED,
"The new package is assigned a different UID than it previously held.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERSION_DOWNGRADE,
"The new package has an older version code than the currently installed package.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_NOT_APK,
"The parser was given a path that is not a file, or does not end with the " +
"expected '.apk' extension.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"the parser was unable to retrieve the AndroidManifest.xml file.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"The parser encountered an unexpected exception.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"The parser did not find any certificates in the .apk.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
"The parser found inconsistent certificates on the files in the .apk.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"The parser encountered a CertificateEncodingException in one of the files in " +
"the .apk.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
"The parser encountered a bad or missing package name in the manifest.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
"The parser encountered a bad shared user id name in the manifest.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
"The parser encountered some structural problem in the manifest.");
INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_EMPTY,
"The parser did not find any actionable tags (instrumentation or application) " +
"in the manifest.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_INTERNAL_ERROR,
"The system failed to install the package because of system issues.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_USER_RESTRICTED,
"The system failed to install the package because the user is restricted from " +
"installing apps.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_DUPLICATE_PERMISSION,
"The system failed to install the package because it is attempting to define a " +
"permission that is already defined by some existing package.");
INSTALL_RETURN_CODES.put(INSTALL_FAILED_NO_MATCHING_ABIS,
"The system failed to install the package because its packaged native code did " +
"not match any of the ABIs supported by the system.");
}
public static final int DELETE_SUCCEEDED = 1;
public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
public static final int DELETE_FAILED_USER_RESTRICTED = -3;
public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
public static final int DELETE_FAILED_ABORTED = -5;
private static final HashMap<Integer, String> UNINSTALL_RETURN_CODES;
static {
// Descriptions extracted from the source code comments in AOSP
UNINSTALL_RETURN_CODES = new HashMap<>();
UNINSTALL_RETURN_CODES.put(DELETE_SUCCEEDED,
"Success");
UNINSTALL_RETURN_CODES.put(DELETE_FAILED_INTERNAL_ERROR,
" the system failed to delete the package for an unspecified reason.");
UNINSTALL_RETURN_CODES.put(DELETE_FAILED_DEVICE_POLICY_MANAGER,
"the system failed to delete the package because it is the active " +
"DevicePolicy manager.");
UNINSTALL_RETURN_CODES.put(DELETE_FAILED_USER_RESTRICTED,
"the system failed to delete the package since the user is restricted.");
UNINSTALL_RETURN_CODES.put(DELETE_FAILED_OWNER_BLOCKED,
"the system failed to delete the package because a profile or " +
"device owner has marked the package as uninstallable.");
}
public PrivilegedInstaller(Context context, Apk apk) {
super(context, apk);
}
public static boolean isExtensionInstalled(Context context) {
PackageManager pm = context.getPackageManager();
try {
pm.getPackageInfo(PRIVILEGED_EXTENSION_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static int isExtensionInstalledCorrectly(Context context) {
// check if installed
if (!isExtensionInstalled(context)) {
return IS_EXTENSION_INSTALLED_NO;
}
ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
}
public void onServiceDisconnected(ComponentName name) {
}
};
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
// try to connect to check for signature
try {
context.getApplicationContext().bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
Log.e(TAG, "IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM", e);
return IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM;
}
return IS_EXTENSION_INSTALLED_YES;
}
/**
* Extension has privileged permissions and preference is enabled?
*/
public static boolean isDefault(Context context) {
return Preferences.get().isPrivilegedInstallerEnabled()
&& isExtensionInstalledCorrectly(context) == IS_EXTENSION_INSTALLED_YES;
}
@Override
protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri) {
ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
@Override
public void handleResult(String packageName, int returnCode) throws RemoteException {
if (returnCode == INSTALL_SUCCEEDED) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_COMPLETE);
} else {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
"Error " + returnCode + ": "
+ INSTALL_RETURN_CODES.get(returnCode));
}
}
};
try {
boolean hasPermissions = privService.hasPrivilegedPermissions();
if (!hasPermissions) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
context.getString(R.string.system_install_denied_permissions));
return;
}
privService.installPackage(localApkUri, ACTION_INSTALL_REPLACE_EXISTING,
null, callback);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException", e);
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
"connecting to privileged service failed");
}
}
public void onServiceDisconnected(ComponentName name) {
}
};
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
Context.BIND_AUTO_CREATE);
}
@Override
protected void uninstallPackage() {
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
@Override
public void handleResult(String packageName, int returnCode) throws RemoteException {
if (returnCode == DELETE_SUCCEEDED) {
sendBroadcastUninstall(ACTION_UNINSTALL_COMPLETE);
} else {
sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED,
"Error " + returnCode + ": "
+ UNINSTALL_RETURN_CODES.get(returnCode));
}
}
};
try {
boolean hasPermissions = privService.hasPrivilegedPermissions();
if (!hasPermissions) {
sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED,
context.getString(R.string.system_install_denied_permissions));
return;
}
privService.deletePackage(apk.packageName, 0, callback);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException", e);
sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED,
"connecting to privileged service failed");
}
}
public void onServiceDisconnected(ComponentName name) {
}
};
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
Context.BIND_AUTO_CREATE);
}
@Override
protected boolean isUnattended() {
return true;
}
@Override
protected boolean supportsContentUri() {
// TODO: correct?
return false;
}
}