Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e272b25a authored by Suprabh Shukla's avatar Suprabh Shukla
Browse files

Enforced app op permission in package installer

Apps targetting O and above need to declare REQUEST_INSTALL_PACKAGES
permission in order to start package installer. Only apps exempt are
downloads provider and document manager apps. And only these apps can
use EXTRA_ORIGINATING_UID to blame other uids for app installs. Also
added a special wraning dialog for when system cannot identify the
source of the package.

Test: gts-tradefed run gts -m ExternalSourcesNegative

Bug: 35916776,
     33351244
Change-Id: I74b242355807502c4e1bf0e7dbdb9845dc725e48
parent 97935a7c
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -347,6 +347,33 @@
    <!-- Text to show in warning dialog on the phone when the app source is not trusted [CHAR LIMIT=NONE] -->
    <string name="untrusted_external_source_warning" product="default">For your security, your phone is not allowed to install unknown apps from this source.</string>

    <!-- Text to show in warning dialog on the phone when the app source cannot be identified [CHAR LIMIT=NONE] -->
    <string name="anonymous_source_warning" product="default">
        Your phone and personal data are more vulnerable
        to attack by unknown apps. By installing this app, you
        agree that you are responsible for any damage to your
        phone or loss of data that may result from its use.
    </string>

    <!-- Text to show in warning dialog on the tablet when the app source cannot be identified [CHAR LIMIT=NONE] -->
    <string name="anonymous_source_warning" product="tablet">
        Your tablet and personal data are more vulnerable
        to attack by unknown apps. By installing this app, you
        agree that you are responsible for any damage to your
        tablet or loss of data that may result from its use.
    </string>

    <!-- Text to show in warning dialog on the tv when the app source cannot be identified [CHAR LIMIT=NONE] -->
    <string name="anonymous_source_warning" product="tv">
        Your TV and personal data are more vulnerable
        to attack by unknown apps. By installing this app, you
        agree that you are responsible for any damage to your
        TV or loss of data that may result from its use.
    </string>

    <!-- Label for button to continue install of an app whose source cannot be identified [CHAR LIMIT=40] -->
    <string name="anonymous_source_continue">Continue</string>

    <!-- Label for button to open manage external sources settings [CHAR LIMIT=45] -->
    <string name="external_sources_settings">Settings</string>

+106 −52
Original line number Diff line number Diff line
@@ -16,16 +16,20 @@

package com.android.packageinstaller;

import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.IActivityManager;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.VerificationParams;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;

@@ -39,12 +43,15 @@ public class InstallStart extends Activity {
    private static final String LOG_TAG = InstallStart.class.getSimpleName();

    private static final String SCHEME_CONTENT = "content";
    private static final String DOWNLOADS_AUTHORITY = "downloads";
    private IActivityManager mIActivityManager;
    private IPackageManager mIPackageManager;
    private boolean mAbortInstall = false;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mIPackageManager = AppGlobals.getPackageManager();
        Intent intent = getIntent();
        String callingPackage = getCallingPackage();

@@ -58,7 +65,26 @@ public class InstallStart extends Activity {
        }

        ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
        int originatingUid = getOriginatingUid(sourceInfo);
        final int originatingUid = getOriginatingUid(sourceInfo);

        if (originatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
            final int targetSdkVersion = getMaxTargetSdkVersionForUid(originatingUid);
            if (targetSdkVersion < 0) {
                Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid);
                // Invalid originating uid supplied. Abort install.
                mAbortInstall = true;
            } else if (targetSdkVersion >= Build.VERSION_CODES.O && !declaresAppOpPermission(
                    originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
                Log.e(LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission "
                        + Manifest.permission.REQUEST_INSTALL_PACKAGES);
                mAbortInstall = true;
            }
        }
        if (mAbortInstall) {
            setResult(RESULT_CANCELED);
            finish();
            return;
        }

        Intent nextActivity = new Intent(intent);
        nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
@@ -97,6 +123,40 @@ public class InstallStart extends Activity {
        finish();
    }

    private boolean declaresAppOpPermission(int uid, String permission) {
        try {
            final String[] packages = mIPackageManager.getAppOpPermissionPackages(permission);
            for (String packageName : packages) {
                try {
                    if (uid == getPackageManager().getPackageUid(packageName, 0)) {
                        return true;
                    }
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore and try the next package
                }
            }
        } catch (RemoteException rexc) {
            // If remote package manager cannot be reached, install will likely fail anyway.
        }
        return false;
    }

    private int getMaxTargetSdkVersionForUid(int uid) {
        final String[] packages = getPackageManager().getPackagesForUid(uid);
        int targetSdkVersion = -1;
        if (packages != null) {
            for (String packageName : packages) {
                try {
                    ApplicationInfo info = getPackageManager().getApplicationInfo(packageName, 0);
                    targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore and try the next package
                }
            }
        }
        return targetSdkVersion;
    }

    /**
     * @return the ApplicationInfo for the installation source (the calling package), if available
     */
@@ -112,66 +172,60 @@ public class InstallStart extends Activity {
    }

    /**
     * Get the originating uid if possible, or VerificationParams.NO_UID if not available
     * Get the originating uid if possible, or
     * {@link android.content.pm.PackageInstaller.SessionParams#UID_UNKNOWN} if not available
     *
     * @param sourceInfo The source of this installation
     *
     * @return The UID of the installation source or VerificationParams.NO_UID
     * @return The UID of the installation source or UID_UNKNOWN
     */
    private int getOriginatingUid(@Nullable ApplicationInfo sourceInfo) {
        // The originating uid from the intent. We only trust/use this if it comes from a
        // system application
        int uidFromIntent = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                VerificationParams.NO_UID);

        // Get the source info from the calling package, if available. This will be the
        // definitive calling package, but it only works if the intent was started using
        // startActivityForResult,
        if (sourceInfo != null) {
            if (uidFromIntent != VerificationParams.NO_UID &&
                    (sourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
                return uidFromIntent;

            }
            // We either didn't get a uid in the intent, or we don't trust it. Use the
            // uid of the calling package instead.
            return sourceInfo.uid;
        }
        // The originating uid from the intent. We only trust/use this if it comes from either
        // the document manager app or the downloads provider
        final int uidFromIntent = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                PackageInstaller.SessionParams.UID_UNKNOWN);

        // We couldn't get the specific calling package. Let's get the uid instead
        int callingUid;
        final int callingUid;
        if (sourceInfo != null) {
            callingUid = sourceInfo.uid;
        } else {
            try {
                callingUid = getIActivityManager()
                        .getLaunchedFromUid(getActivityToken());
        } catch (android.os.RemoteException ex) {
            Log.w(LOG_TAG, "Could not determine the launching uid.");
            // nothing else we can do
            return VerificationParams.NO_UID;
        }

        // If we got a uid from the intent, we need to verify that the caller is a
        // privileged system package before we use it
        if (uidFromIntent != VerificationParams.NO_UID) {
            String[] callingPackages = getPackageManager().getPackagesForUid(callingUid);
            if (callingPackages != null) {
                for (String packageName: callingPackages) {
            } catch (RemoteException ex) {
                // Cannot reach ActivityManager. Aborting install.
                Log.e(LOG_TAG, "Could not determine the launching uid.");
                mAbortInstall = true;
                return PackageInstaller.SessionParams.UID_UNKNOWN;
            }
        }
        try {
                        ApplicationInfo applicationInfo =
                                getPackageManager().getApplicationInfo(packageName, 0);

                        if ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
                                != 0) {
            if (mIPackageManager.checkUidPermission(Manifest.permission.MANAGE_DOCUMENTS,
                    callingUid) == PackageManager.PERMISSION_GRANTED) {
                return uidFromIntent;
            }
                    } catch (PackageManager.NameNotFoundException ex) {
                        // ignore it, and try the next package
        } catch (RemoteException rexc) {
            // Ignore. Should not happen.
        }
        if (isSystemDownloadsProvider(callingUid)) {
            return uidFromIntent;
        }
        // We don't trust uid from the intent. Use the calling uid instead.
        return callingUid;
    }

    private boolean isSystemDownloadsProvider(int uid) {
        final String downloadProviderPackage = getPackageManager().resolveContentProvider(
                DOWNLOADS_AUTHORITY, 0).getComponentName().getPackageName();
        if (downloadProviderPackage == null) {
            return false;
        }
        try {
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                    downloadProviderPackage, 0);
            return (applicationInfo.isSystemApp() && uid == applicationInfo.uid);
        } catch (PackageManager.NameNotFoundException ex) {
            return false;
        }
        // We either didn't get a uid from the intent, or we don't trust it. Use the
        // calling uid instead.
        return callingUid;
    }

    private IActivityManager getIActivityManager() {
+31 −9
Original line number Diff line number Diff line
@@ -37,7 +37,6 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageParser;
import android.content.pm.PackageUserState;
import android.content.pm.VerificationParams;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -87,7 +86,7 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
    private Uri mPackageURI;
    private Uri mOriginatingURI;
    private Uri mReferrerURI;
    private int mOriginatingUid = VerificationParams.NO_UID;
    private int mOriginatingUid = PackageInstaller.SessionParams.UID_UNKNOWN;
    private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid

    private boolean localLOGV = false;
@@ -122,6 +121,7 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
    private static final int DLG_OUT_OF_SPACE = DLG_BASE + 3;
    private static final int DLG_INSTALL_ERROR = DLG_BASE + 4;
    private static final int DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER = DLG_BASE + 5;
    private static final int DLG_ANONYMOUS_SOURCE = DLG_BASE + 6;
    private static final int DLG_NOT_SUPPORTED_ON_WEAR = DLG_BASE + 7;
    private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 8;

@@ -273,6 +273,8 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
                        R.string.unknown_apps_user_restriction_dlg_text);
            case DLG_EXTERNAL_SOURCE_BLOCKED:
                return ExternalSourcesBlockedDialog.newInstance(mOriginatingPackage);
            case DLG_ANONYMOUS_SOURCE:
                return AnonymousSourceDialog.newInstance();
        }
        return null;
    }
@@ -320,7 +322,7 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
            if (mSourceInfo != null) {
                if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
                        != 0) {
                    // Privileged apps are not considered an unknown source.
                    // Privileged apps can bypass unknown sources check if they want.
                    return false;
                }
            }
@@ -388,9 +390,9 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
        mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
        mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
        mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                VerificationParams.NO_UID);
        mOriginatingPackage = (mOriginatingUid != VerificationParams.NO_UID) ? getPackageNameForUid(
                mOriginatingUid) : null;
                PackageInstaller.SessionParams.UID_UNKNOWN);
        mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
                ? getPackageNameForUid(mOriginatingUid) : null;


        final Uri packageUri;
@@ -489,8 +491,8 @@ public class PackageInstallerActivity extends Activity implements OnClickListene

    private void handleUnknownSources() {
        if (mOriginatingPackage == null) {
            Log.e(TAG, "No source package name for external install request. Aborting install");
            finish();
            Log.i(TAG, "No source found for package " + mPkgInfo.packageName);
            showDialogInner(DLG_ANONYMOUS_SOURCE);
            return;
        }
        int appOpMode = mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
@@ -630,7 +632,7 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
        if (mReferrerURI != null) {
            newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
        }
        if (mOriginatingUid != VerificationParams.NO_UID) {
        if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
            newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
        }
        if (installerPackageName != null) {
@@ -672,6 +674,26 @@ public class PackageInstallerActivity extends Activity implements OnClickListene
        }
    }

    /**
     * Dialog to show when the source of apk can not be identified
     */
    public static class AnonymousSourceDialog extends DialogFragment {
        static AnonymousSourceDialog newInstance() {
            return new AnonymousSourceDialog();
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return new AlertDialog.Builder(getActivity())
                    .setMessage(R.string.anonymous_source_warning)
                    .setPositiveButton(R.string.anonymous_source_continue,
                            ((dialog, which) -> ((PackageInstallerActivity) getActivity())
                                    .initiateInstall()))
                    .setNegativeButton(R.string.cancel, ((dialog, which) -> getActivity().finish()))
                    .create();
        }
    }

    /**
     * An error dialog shown when the app is not supported on wear
     */
+0 −137
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.packageinstaller;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.app.IActivityManager;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.os.IBinder;

import com.android.packageinstaller.shadows.ShadowPackageInstaller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;

/**
 * Unit-tests for {@link InstallStart}.
 */
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "packages/apps/PackageInstaller/AndroidManifest.xml",
        shadows = ShadowPackageInstaller.class,
        sdk = 23)
public class InstallStartTest {
    private static final int TEST_UID = 12345;
    private static final int TEST_SESSION_ID = 12;
    private static final String TEST_INSTALLER = "com.test.installer";

    @Mock
    private IActivityManager mMockActivityManager;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        when(mMockActivityManager.getLaunchedFromUid(any(IBinder.class)))
                .thenReturn(TEST_UID);
    }

    @Test
    public void testActionConfirmPermission() {
        // GIVEN the activity was launched with ACTION_CONFIRM_PERMISSION and no extras
        Intent launchIntent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS);
        ActivityController<InstallStart> activityController =
                Robolectric.buildActivity(InstallStart.class).withIntent(launchIntent);
        activityController.get().injectIActivityManager(mMockActivityManager);

        // WHEN onCreate is called
        activityController.create();

        // THEN the activity should be finishing itself, as its no UI activity
        assertTrue(shadowOf(activityController.get()).isFinishing());

        // THEN PackageInstallerActivity should be launched
        assertEquals(PackageInstallerActivity.class.getName(),
                shadowOf(activityController.get()).getNextStartedActivity().getComponent()
                        .getClassName());
    }

    @Test
    public void testActionConfirmPermissionWithExtra() {
        // GIVEN the activity was launched with ACTION_CONFIRM_PERMISSION and
        //       EXTRA_NOT_UNKNOWN_SOURCE set to true
        Intent launchIntent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS)
                .putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
        ActivityController<InstallStart> activityController =
                Robolectric.buildActivity(InstallStart.class).withIntent(launchIntent);
        activityController.get().injectIActivityManager(mMockActivityManager);

        // WHEN onCreate is called
        activityController.create();

        // THEN the activity should be finishing itself, as its no UI activity
        assertTrue(shadowOf(activityController.get()).isFinishing());

        // THEN PackageInstallerActivity should be launched
        Intent intent = shadowOf(activityController.get()).getNextStartedActivity();
        assertEquals(PackageInstallerActivity.class.getName(),
                intent.getComponent().getClassName());
        assertTrue(intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false));
    }

    @Test
    public void testActionConfirmPermissionWithSessionId() {
        // GIVEN the activity was launched with ACTION_CONFIRM_PERMISSION and
        //       EXTRA_SESSION_ID set
        Intent launchIntent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS)
                .putExtra(PackageInstaller.EXTRA_SESSION_ID, TEST_SESSION_ID);
        ActivityController<InstallStart> activityController =
                Robolectric.buildActivity(InstallStart.class).withIntent(launchIntent);
        activityController.get().injectIActivityManager(mMockActivityManager);

        // GIVEN that a PackageInstallerSession with that session id exists
        PackageInstaller.SessionInfo session = new PackageInstaller.SessionInfo();
        session.sessionId = TEST_SESSION_ID;
        session.installerPackageName = TEST_INSTALLER;
        ShadowPackageInstaller.putSessionInfo(session);

        // WHEN onCreate is called
        activityController.create();

        // THEN the activity should be finishing itself, as its no UI activity
        assertTrue(shadowOf(activityController.get()).isFinishing());

        // THEN PackageInstallerActivity should be launched
        Intent intent = shadowOf(activityController.get()).getNextStartedActivity();
        assertEquals(PackageInstallerActivity.class.getName(),
                intent.getComponent().getClassName());
        assertEquals(TEST_INSTALLER,
                intent.getStringExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE));
    }
}