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