Home | History | Annotate | Download | only in appsecurity
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.cts.appsecurity;
     18 
     19 import com.android.cts.tradefed.build.CtsBuildHelper;
     20 import com.android.ddmlib.Log;
     21 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
     22 import com.android.ddmlib.testrunner.TestIdentifier;
     23 import com.android.tradefed.build.IBuildInfo;
     24 import com.android.tradefed.device.DeviceNotAvailableException;
     25 import com.android.tradefed.device.ITestDevice;
     26 import com.android.tradefed.result.CollectingTestListener;
     27 import com.android.tradefed.result.TestResult;
     28 import com.android.tradefed.result.TestRunResult;
     29 import com.android.tradefed.result.TestResult.TestStatus;
     30 import com.android.tradefed.testtype.DeviceTestCase;
     31 import com.android.tradefed.testtype.IBuildReceiver;
     32 
     33 import java.io.File;
     34 import java.io.FileNotFoundException;
     35 import java.util.Map;
     36 
     37 /**
     38  * Set of tests that verify various security checks involving multiple apps are properly enforced.
     39  */
     40 public class AppSecurityTests extends DeviceTestCase implements IBuildReceiver {
     41 
     42     // testSharedUidDifferentCerts constants
     43     private static final String SHARED_UI_APK = "CtsSharedUidInstall.apk";
     44     private static final String SHARED_UI_PKG = "com.android.cts.shareuidinstall";
     45     private static final String SHARED_UI_DIFF_CERT_APK = "CtsSharedUidInstallDiffCert.apk";
     46     private static final String SHARED_UI_DIFF_CERT_PKG =
     47         "com.android.cts.shareuidinstalldiffcert";
     48 
     49     // testAppUpgradeDifferentCerts constants
     50     private static final String SIMPLE_APP_APK = "CtsSimpleAppInstall.apk";
     51     private static final String SIMPLE_APP_PKG = "com.android.cts.simpleappinstall";
     52     private static final String SIMPLE_APP_DIFF_CERT_APK = "CtsSimpleAppInstallDiffCert.apk";
     53 
     54     // testAppFailAccessPrivateData constants
     55     private static final String APP_WITH_DATA_APK = "CtsAppWithData.apk";
     56     private static final String APP_WITH_DATA_PKG = "com.android.cts.appwithdata";
     57     private static final String APP_WITH_DATA_CLASS =
     58             "com.android.cts.appwithdata.CreatePrivateDataTest";
     59     private static final String APP_WITH_DATA_CREATE_METHOD =
     60             "testCreatePrivateData";
     61     private static final String APP_WITH_DATA_CHECK_NOEXIST_METHOD =
     62             "testEnsurePrivateDataNotExist";
     63     private static final String APP_ACCESS_DATA_APK = "CtsAppAccessData.apk";
     64     private static final String APP_ACCESS_DATA_PKG = "com.android.cts.appaccessdata";
     65 
     66     // External storage constants
     67     private static final String EXTERNAL_STORAGE_APP_APK = "CtsExternalStorageApp.apk";
     68     private static final String EXTERNAL_STORAGE_APP_PKG = "com.android.cts.externalstorageapp";
     69     private static final String EXTERNAL_STORAGE_APP_CLASS = EXTERNAL_STORAGE_APP_PKG
     70             + ".ExternalStorageTest";
     71     private static final String WRITE_EXTERNAL_STORAGE_APP_APK = "CtsWriteExternalStorageApp.apk";
     72     private static final String
     73             WRITE_EXTERNAL_STORAGE_APP_PKG = "com.android.cts.writeexternalstorageapp";
     74     private static final String WRITE_EXTERNAL_STORAGE_APP_CLASS = WRITE_EXTERNAL_STORAGE_APP_PKG
     75             + ".WriteExternalStorageTest";
     76 
     77     // testInstrumentationDiffCert constants
     78     private static final String TARGET_INSTRUMENT_APK = "CtsTargetInstrumentationApp.apk";
     79     private static final String TARGET_INSTRUMENT_PKG = "com.android.cts.targetinstrumentationapp";
     80     private static final String INSTRUMENT_DIFF_CERT_APK = "CtsInstrumentationAppDiffCert.apk";
     81     private static final String INSTRUMENT_DIFF_CERT_PKG =
     82         "com.android.cts.instrumentationdiffcertapp";
     83 
     84     // testPermissionDiffCert constants
     85     private static final String DECLARE_PERMISSION_APK = "CtsPermissionDeclareApp.apk";
     86     private static final String DECLARE_PERMISSION_PKG = "com.android.cts.permissiondeclareapp";
     87     private static final String PERMISSION_DIFF_CERT_APK = "CtsUsePermissionDiffCert.apk";
     88     private static final String PERMISSION_DIFF_CERT_PKG =
     89         "com.android.cts.usespermissiondiffcertapp";
     90 
     91     private static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE";
     92 
     93     private static final String LOG_TAG = "AppSecurityTests";
     94 
     95     private CtsBuildHelper mCtsBuild;
     96 
     97     /**
     98      * {@inheritDoc}
     99      */
    100     @Override
    101     public void setBuild(IBuildInfo buildInfo) {
    102         mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo);
    103     }
    104 
    105     private File getTestAppFile(String fileName) throws FileNotFoundException {
    106         return mCtsBuild.getTestApp(fileName);
    107     }
    108 
    109     @Override
    110     protected void setUp() throws Exception {
    111         super.setUp();
    112         // ensure build has been set before test is run
    113         assertNotNull(mCtsBuild);
    114     }
    115 
    116     /**
    117      * Test that an app that declares the same shared uid as an existing app, cannot be installed
    118      * if it is signed with a different certificate.
    119      */
    120     public void testSharedUidDifferentCerts() throws Exception {
    121         Log.i(LOG_TAG, "installing apks with shared uid, but different certs");
    122         try {
    123             // cleanup test apps that might be installed from previous partial test run
    124             getDevice().uninstallPackage(SHARED_UI_PKG);
    125             getDevice().uninstallPackage(SHARED_UI_DIFF_CERT_PKG);
    126 
    127             String installResult = getDevice().installPackage(getTestAppFile(SHARED_UI_APK),
    128                     false);
    129             assertNull(String.format("failed to install shared uid app, Reason: %s", installResult),
    130                     installResult);
    131             installResult = getDevice().installPackage(getTestAppFile(SHARED_UI_DIFF_CERT_APK),
    132                     false);
    133             assertNotNull("shared uid app with different cert than existing app installed " +
    134                     "successfully", installResult);
    135             assertEquals("INSTALL_FAILED_SHARED_USER_INCOMPATIBLE", installResult);
    136         }
    137         finally {
    138             getDevice().uninstallPackage(SHARED_UI_PKG);
    139             getDevice().uninstallPackage(SHARED_UI_DIFF_CERT_PKG);
    140         }
    141     }
    142 
    143     /**
    144      * Test that an app update cannot be installed over an existing app if it has a different
    145      * certificate.
    146      */
    147     public void testAppUpgradeDifferentCerts() throws Exception {
    148         Log.i(LOG_TAG, "installing app upgrade with different certs");
    149         try {
    150             // cleanup test app that might be installed from previous partial test run
    151             getDevice().uninstallPackage(SIMPLE_APP_PKG);
    152 
    153             String installResult = getDevice().installPackage(getTestAppFile(SIMPLE_APP_APK),
    154                     false);
    155             assertNull(String.format("failed to install simple app. Reason: %s", installResult),
    156                     installResult);
    157             installResult = getDevice().installPackage(getTestAppFile(SIMPLE_APP_DIFF_CERT_APK),
    158                     true /* reinstall */);
    159             assertNotNull("app upgrade with different cert than existing app installed " +
    160                     "successfully", installResult);
    161             assertEquals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES", installResult);
    162         }
    163         finally {
    164             getDevice().uninstallPackage(SIMPLE_APP_PKG);
    165         }
    166     }
    167 
    168     /**
    169      * Test that an app cannot access another app's private data.
    170      */
    171     public void testAppFailAccessPrivateData() throws Exception {
    172         Log.i(LOG_TAG, "installing app that attempts to access another app's private data");
    173         try {
    174             // cleanup test app that might be installed from previous partial test run
    175             getDevice().uninstallPackage(APP_WITH_DATA_PKG);
    176             getDevice().uninstallPackage(APP_ACCESS_DATA_PKG);
    177 
    178             String installResult = getDevice().installPackage(getTestAppFile(APP_WITH_DATA_APK),
    179                     false);
    180             assertNull(String.format("failed to install app with data. Reason: %s", installResult),
    181                     installResult);
    182             // run appwithdata's tests to create private data
    183             assertTrue("failed to create app's private data", runDeviceTests(APP_WITH_DATA_PKG,
    184                     APP_WITH_DATA_CLASS, APP_WITH_DATA_CREATE_METHOD));
    185 
    186             installResult = getDevice().installPackage(getTestAppFile(APP_ACCESS_DATA_APK),
    187                     false);
    188             assertNull(String.format("failed to install app access data. Reason: %s",
    189                     installResult), installResult);
    190             // run appaccessdata's tests which attempt to access appwithdata's private data
    191             assertTrue("could access app's private data", runDeviceTests(APP_ACCESS_DATA_PKG));
    192         }
    193         finally {
    194             getDevice().uninstallPackage(APP_WITH_DATA_PKG);
    195             getDevice().uninstallPackage(APP_ACCESS_DATA_PKG);
    196         }
    197     }
    198 
    199     /**
    200      * Test behavior when
    201      * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} is unenforced.
    202      */
    203     public void testReadExternalStorageUnenforced() throws Exception {
    204         try {
    205             getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
    206             getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
    207 
    208             // stage test file on external storage
    209             getDevice().pushString("CAEK", "/sdcard/meow");
    210 
    211             // mark permission as not enforced
    212             setPermissionEnforced(getDevice(), READ_EXTERNAL_STORAGE, false);
    213 
    214             // install apps and run test
    215             assertNull(getDevice()
    216                     .installPackage(getTestAppFile(EXTERNAL_STORAGE_APP_APK), false));
    217             assertNull(getDevice()
    218                     .installPackage(getTestAppFile(WRITE_EXTERNAL_STORAGE_APP_APK), false));
    219 
    220             // normal app should be able to read
    221             assertTrue("Normal app unable to read external storage", runDeviceTests(
    222                     EXTERNAL_STORAGE_APP_PKG, EXTERNAL_STORAGE_APP_CLASS,
    223                     "testReadExternalStorage"));
    224 
    225             // WRITE_EXTERNAL app should be able to read and write
    226             assertTrue("WRITE_EXTERNAL app unable to read external storage", runDeviceTests(
    227                     WRITE_EXTERNAL_STORAGE_APP_PKG, WRITE_EXTERNAL_STORAGE_APP_CLASS,
    228                     "testReadExternalStorage"));
    229             assertTrue("WRITE_EXTERNAL app unable to write external storage", runDeviceTests(
    230                     WRITE_EXTERNAL_STORAGE_APP_PKG, WRITE_EXTERNAL_STORAGE_APP_CLASS,
    231                     "testWriteExternalStorage"));
    232 
    233         } finally {
    234             getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
    235             getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
    236         }
    237     }
    238 
    239     /**
    240      * Test that uninstall of an app removes its private data.
    241      */
    242     public void testUninstallRemovesData() throws Exception {
    243         Log.i(LOG_TAG, "Uninstalling app, verifying data is removed.");
    244         try {
    245             // cleanup test app that might be installed from previous partial test run
    246             getDevice().uninstallPackage(APP_WITH_DATA_PKG);
    247 
    248             String installResult = getDevice().installPackage(getTestAppFile(APP_WITH_DATA_APK),
    249                     false);
    250             assertNull(String.format("failed to install app with data. Reason: %s", installResult),
    251                     installResult);
    252             // run appwithdata's tests to create private data
    253             assertTrue("failed to create app's private data", runDeviceTests(APP_WITH_DATA_PKG,
    254                     APP_WITH_DATA_CLASS, APP_WITH_DATA_CREATE_METHOD));
    255 
    256             getDevice().uninstallPackage(APP_WITH_DATA_PKG);
    257 
    258             installResult = getDevice().installPackage(getTestAppFile(APP_WITH_DATA_APK),
    259                     false);
    260             assertNull(String.format("failed to install app with data second time. Reason: %s",
    261                     installResult), installResult);
    262             // run appwithdata's 'check if file exists' test
    263             assertTrue("app's private data still exists after install", runDeviceTests(
    264                     APP_WITH_DATA_PKG, APP_WITH_DATA_CLASS, APP_WITH_DATA_CHECK_NOEXIST_METHOD));
    265 
    266         }
    267         finally {
    268             getDevice().uninstallPackage(APP_WITH_DATA_PKG);
    269         }
    270     }
    271 
    272     /**
    273      * Test that an app cannot instrument another app that is signed with different certificate.
    274      */
    275     public void testInstrumentationDiffCert() throws Exception {
    276         Log.i(LOG_TAG, "installing app that attempts to instrument another app");
    277         try {
    278             // cleanup test app that might be installed from previous partial test run
    279             getDevice().uninstallPackage(TARGET_INSTRUMENT_PKG);
    280             getDevice().uninstallPackage(INSTRUMENT_DIFF_CERT_PKG);
    281 
    282             String installResult = getDevice().installPackage(
    283                     getTestAppFile(TARGET_INSTRUMENT_APK), false);
    284             assertNull(String.format("failed to install target instrumentation app. Reason: %s",
    285                     installResult), installResult);
    286 
    287             // the app will install, but will get error at runtime when starting instrumentation
    288             installResult = getDevice().installPackage(getTestAppFile(INSTRUMENT_DIFF_CERT_APK),
    289                     false);
    290             assertNull(String.format(
    291                     "failed to install instrumentation app with diff cert. Reason: %s",
    292                     installResult), installResult);
    293             // run INSTRUMENT_DIFF_CERT_PKG tests
    294             // this test will attempt to call startInstrumentation directly and verify
    295             // SecurityException is thrown
    296             assertTrue("running instrumentation with diff cert unexpectedly succeeded",
    297                     runDeviceTests(INSTRUMENT_DIFF_CERT_PKG));
    298         }
    299         finally {
    300             getDevice().uninstallPackage(TARGET_INSTRUMENT_PKG);
    301             getDevice().uninstallPackage(INSTRUMENT_DIFF_CERT_PKG);
    302         }
    303     }
    304 
    305     /**
    306      * Test that an app cannot use a signature-enforced permission if it is signed with a different
    307      * certificate than the app that declared the permission.
    308      */
    309     public void testPermissionDiffCert() throws Exception {
    310         Log.i(LOG_TAG, "installing app that attempts to use permission of another app");
    311         try {
    312             // cleanup test app that might be installed from previous partial test run
    313             getDevice().uninstallPackage(DECLARE_PERMISSION_PKG);
    314             getDevice().uninstallPackage(PERMISSION_DIFF_CERT_PKG);
    315 
    316             String installResult = getDevice().installPackage(
    317                     getTestAppFile(DECLARE_PERMISSION_APK), false);
    318             assertNull(String.format("failed to install declare permission app. Reason: %s",
    319                     installResult), installResult);
    320 
    321             // the app will install, but will get error at runtime
    322             installResult = getDevice().installPackage(getTestAppFile(PERMISSION_DIFF_CERT_APK),
    323                     false);
    324             assertNull(String.format("failed to install permission app with diff cert. Reason: %s",
    325                     installResult), installResult);
    326             // run PERMISSION_DIFF_CERT_PKG tests which try to access the permission
    327             TestRunResult result = doRunTests(PERMISSION_DIFF_CERT_PKG, null, null);
    328             assertDeviceTestsPass(result);
    329         }
    330         finally {
    331             getDevice().uninstallPackage(DECLARE_PERMISSION_PKG);
    332             getDevice().uninstallPackage(PERMISSION_DIFF_CERT_PKG);
    333         }
    334     }
    335 
    336     /**
    337      * Helper method that checks that all tests in given result passed, and attempts to generate
    338      * a meaningful error message if they failed.
    339      *
    340      * @param result
    341      */
    342     private void assertDeviceTestsPass(TestRunResult result) {
    343         // TODO: consider rerunning if this occurred
    344         assertFalse(String.format("Failed to successfully run device tests for %s. Reason: %s",
    345                 result.getName(), result.getRunFailureMessage()), result.isRunFailure());
    346 
    347         if (result.hasFailedTests()) {
    348             // build a meaningful error message
    349             StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
    350             for (Map.Entry<TestIdentifier, TestResult> resultEntry :
    351                 result.getTestResults().entrySet()) {
    352                 if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
    353                     errorBuilder.append(resultEntry.getKey().toString());
    354                     errorBuilder.append(":\n");
    355                     errorBuilder.append(resultEntry.getValue().getStackTrace());
    356                 }
    357             }
    358             fail(errorBuilder.toString());
    359         }
    360     }
    361 
    362     /**
    363      * Helper method that will the specified packages tests on device.
    364      *
    365      * @param pkgName Android application package for tests
    366      * @return <code>true</code> if all tests passed.
    367      * @throws DeviceNotAvailableException if connection to device was lost.
    368      */
    369     private boolean runDeviceTests(String pkgName) throws DeviceNotAvailableException {
    370         return runDeviceTests(pkgName, null, null);
    371     }
    372 
    373     /**
    374      * Helper method that will the specified packages tests on device.
    375      *
    376      * @param pkgName Android application package for tests
    377      * @return <code>true</code> if all tests passed.
    378      * @throws DeviceNotAvailableException if connection to device was lost.
    379      */
    380     private boolean runDeviceTests(String pkgName, String testClassName, String testMethodName)
    381             throws DeviceNotAvailableException {
    382         TestRunResult runResult = doRunTests(pkgName, testClassName, testMethodName);
    383         return !runResult.hasFailedTests();
    384     }
    385 
    386     /**
    387      * Helper method to run tests and return the listener that collected the results.
    388      *
    389      * @param pkgName Android application package for tests
    390      * @return the {@link TestRunResult}
    391      * @throws DeviceNotAvailableException if connection to device was lost.
    392      */
    393     private TestRunResult doRunTests(String pkgName, String testClassName,
    394             String testMethodName) throws DeviceNotAvailableException {
    395 
    396         RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName,
    397                 getDevice().getIDevice());
    398         if (testClassName != null && testMethodName != null) {
    399             testRunner.setMethodName(testClassName, testMethodName);
    400         }
    401         CollectingTestListener listener = new CollectingTestListener();
    402         getDevice().runInstrumentationTests(testRunner, listener);
    403         return listener.getCurrentRunResults();
    404     }
    405 
    406     private static void setPermissionEnforced(
    407             ITestDevice device, String permission, boolean enforced)
    408             throws DeviceNotAvailableException {
    409         device.executeShellCommand("pm set-permission-enforced " + permission + " "
    410                 + Boolean.toString(enforced));
    411     }
    412 }
    413