Rewrite app-with-activity to match guidelines
Bug: 441727732
Test: manually verified from command line and Android Studio
Change-Id: I1eeb4852da5d6f19b8e24c319bebd24392f79c51
diff --git a/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml b/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml
index 3f2ec78..bc21fae 100644
--- a/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml
+++ b/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml
@@ -25,11 +25,6 @@
android:supportsRtl="true">
<activity
android:name=".PocActivity"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
+ android:exported="true" />
</application>
</manifest>
diff --git a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java
index 059b0ea..37dd0be 100644
--- a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java
+++ b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java
@@ -17,11 +17,11 @@
package com.android.security;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import android.app.Instrumentation;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -29,7 +29,9 @@
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -38,63 +40,114 @@
import java.util.concurrent.atomic.AtomicReference;
/**
- * An example device test that starts an activity and uses broadcasts to wait for the artifact
- * proving vulnerability
+ * ## Test Architecture: Device-Side Logic
+ *
+ * This class runs as an instrumented test on the Android device. It contains a single `@Test`
+ * method that can be run in different "phases" based on arguments from the host.
*/
@RunWith(AndroidJUnit4.class)
public class DeviceTest {
private static final String TAG = DeviceTest.class.getSimpleName();
- Context mContext;
+ private static final int TIMEOUT_MS = 5000;
- /** Test broadcast action */
- public static final String ACTION_BROADCAST = "action_security_test_broadcast";
+ /** Test broadcast action sent from the PocActivity with the result. */
+ public static final String ACTION_BROADCAST = "com.android.security.action.RESULT_BROADCAST";
- /** Broadcast intent extra name for artifacts */
+ /** Broadcast intent extra name for artifacts. */
public static final String INTENT_ARTIFACT = "artifact";
- /** Device test */
- @Test
- public void testDeviceSideMethod() throws Exception {
- mContext = getApplicationContext();
+ private Context mContext;
+ private Instrumentation mInstrumentation;
- AtomicReference<String> actual = new AtomicReference<>();
+ @Before
+ public void setUp() {
+ mContext = getApplicationContext();
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ /**
+ * This is the single entry point for the device-side test. It follows the "Prove the Boundary"
+ * pattern to create a reliable test.
+ */
+ @Test
+ public void testVulnerabilityFailsOnVulnerableDevice() throws Exception {
+ // STEP 1: Assert the Boundary is Intact
+ // Launch the PoC activity without the trigger file present.
+ // The sensitive action should be blocked by the security boundary.
+ Log.d(TAG, "STEP 1: Asserting that the security boundary is intact...");
+ String initialArtifact = launchActivityAndGetResult();
+ assertEquals(
+ "The security boundary should have blocked the action.",
+ "ACCESS_DENIED",
+ initialArtifact);
+ Log.d(TAG, "Success: The security boundary correctly blocked the action.");
+
+ // STEP 2: Trigger the Vulnerability
+ // Create the trigger file that the PoC activity will detect.
+ Log.d(TAG, "STEP 2: Triggering the vulnerability...");
+ mInstrumentation
+ .getUiAutomation()
+ .executeShellCommand("touch " + PocActivity.TRIGGER_FILE_PATH)
+ .close();
+ Log.d(TAG, "Success: Vulnerability trigger created at " + PocActivity.TRIGGER_FILE_PATH);
+
+ // STEP 3: Assert the Boundary is Breached
+ // Relaunch the PoC activity. This time, it should detect the trigger file and the
+ // sensitive action should succeed.
+ Log.d(TAG, "STEP 3: Asserting that the security boundary is breached...");
+ String finalArtifact = launchActivityAndGetResult();
+
+ // The final, critical assertion. If the artifact contains the secret data,
+ // we fail the test and report the artifact as proof. This test SHOULD fail on a
+ // vulnerable device.
+ if ("SECRET_DATA_ACCESSED".equals(finalArtifact)) {
+ fail("Vulnerability proven: Leaked sensitive data: '" + finalArtifact + "'");
+ }
+
+ // If the exploit failed, the artifact will be "ACCESS_DENIED". In this case, the
+ // test will pass, correctly indicating that the device is not vulnerable.
+ assertEquals(
+ "The exploit failed; the security boundary was not bypassed.",
+ "ACCESS_DENIED",
+ finalArtifact);
+ Log.d(
+ TAG,
+ "The device appears to be secure; the exploit did not bypass the security"
+ + " boundary.");
+ }
+
+ /**
+ * Launches the PocActivity, waits for it to send a result broadcast, and returns the artifact.
+ */
+ private String launchActivityAndGetResult() throws InterruptedException {
+ final AtomicReference<String> artifactRef = new AtomicReference<>();
final Semaphore broadcastReceived = new Semaphore(0);
+
BroadcastReceiver broadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- try {
- if (!intent.getAction().equals(ACTION_BROADCAST)) {
- Log.i(
- TAG,
- String.format(
- "got a broadcast that we didn't expect: %s",
- intent.getAction()));
- }
- actual.set(intent.getStringExtra(INTENT_ARTIFACT));
+ if (ACTION_BROADCAST.equals(intent.getAction())) {
+ artifactRef.set(intent.getStringExtra(INTENT_ARTIFACT));
broadcastReceived.release();
- } catch (Exception e) {
- Log.e(TAG, "got an exception when handling broadcast", e);
}
}
};
- IntentFilter filter = new IntentFilter();
- filter.addAction(ACTION_BROADCAST);
- mContext.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_EXPORTED);
+ mContext.registerReceiver(
+ broadcastReceiver, new IntentFilter(ACTION_BROADCAST), Context.RECEIVER_EXPORTED);
- // start the target app
- try {
- Log.d(TAG, "starting local activity");
- Intent newActivityIntent = new Intent(mContext, PocActivity.class);
- newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // this could be startActivityForResult, but is generic for illustrative purposes
- mContext.startActivity(newActivityIntent);
- } finally {
- getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ // Start the target app.
+ Intent newActivityIntent = new Intent(mContext, PocActivity.class);
+ newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(newActivityIntent);
+
+ // Wait for the result from the activity.
+ if (!broadcastReceived.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for result from PocActivity.");
}
- assertTrue(
- "Timed out when getting result from other activity",
- broadcastReceived.tryAcquire(/* TIMEOUT_MS */ 5000, TimeUnit.MILLISECONDS));
- assertEquals("The target artifact should have been 'secure'", "secure", actual.get());
+
+ mContext.unregisterReceiver(broadcastReceiver);
+
+ return artifactRef.get();
}
}
diff --git a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java
index 113bd2b..e69a1c1 100644
--- a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java
+++ b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java
@@ -21,22 +21,65 @@
import android.os.Bundle;
import android.util.Log;
+import java.io.File;
+
+/**
+ * ## Test Architecture: The Proof-of-Concept Application
+ *
+ * This Activity serves as the proof-of-concept (PoC) application. It is **not** the vulnerable
+ * component itself. Instead, it is a tool designed to trigger and demonstrate a vulnerability in the
+ * underlying Android platform or SoC components.
+ *
+ * ### The Mock Vulnerability Trigger
+ * The "vulnerability" this app triggers is simple: if a specific file exists at {@link
+ * #TRIGGER_FILE_PATH}, a sensitive action that should normally be blocked will succeed.
+ *
+ * This simulates a real-world scenario where an attacker-controlled app could perform an action
+ * (like creating a file or sending an intent) that puts a platform component into an insecure
+ * state, allowing the app to bypass a security boundary.
+ *
+ * This Activity is UI-less and designed for automated testing. It performs its check and reports its
+ * result via a broadcast as soon as it is launched, then finishes.
+ */
public class PocActivity extends Activity {
private static final String TAG = PocActivity.class.getSimpleName();
+ // A file path that the host can write to and the app can read.
+ static final String TRIGGER_FILE_PATH = "/data/local/tmp/exploit_trigger.txt";
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- Log.d(TAG, "poc activity started");
- // Collect the artifact representing vulnerability here.
- // Change this to whatever type best fits the vulnerable artifact; consider using a bundle
- // if there are multiple artifacts necessary to prove the security vulnerability.
- String artifact = "vulnerable";
+ boolean isExploitTriggered = new File(TRIGGER_FILE_PATH).exists();
+ Log.d(TAG, "PocActivity started. Exploit triggered: " + isExploitTriggered);
- Intent vulnerabilityDescriptionIntent = new Intent(DeviceTest.ACTION_BROADCAST);
- vulnerabilityDescriptionIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact);
- this.sendBroadcast(vulnerabilityDescriptionIntent);
+ // This is the sensitive action that should be protected by a security boundary.
+ // It is triggered automatically when the Activity is created.
+ attemptSensitiveAction(isExploitTriggered);
+ }
+
+ /**
+ * Attempts to perform the sensitive action. The outcome depends on whether the mock exploit has
+ * been triggered.
+ */
+ private void attemptSensitiveAction(boolean isExploitTriggered) {
+ String artifact;
+ if (isExploitTriggered) {
+ // STEP 3: The exploit has been triggered. The sensitive action now succeeds.
+ // We collect an artifact that proves the vulnerability.
+ artifact = "SECRET_DATA_ACCESSED";
+ Log.d(TAG, "Sensitive action succeeded. Captured artifact: " + artifact);
+ } else {
+ // STEP 2: The exploit has not been triggered. The sensitive action is blocked.
+ artifact = "ACCESS_DENIED";
+ Log.d(TAG, "Sensitive action blocked by security boundary.");
+ }
+
+ // Send the result back to the device-side test.
+ Intent resultIntent = new Intent(DeviceTest.ACTION_BROADCAST);
+ resultIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact);
+ sendBroadcast(resultIntent);
+ finish(); // Close the activity after the action is performed.
}
}
diff --git a/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java b/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java
index 2cc1406..496ef32 100644
--- a/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java
+++ b/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java
@@ -21,10 +21,21 @@
import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import org.junit.Test;
import org.junit.runner.RunWith;
+/**
+ * ## Test Architecture: Host-Side Launcher
+ *
+ * This host-side test is minimal. Its only purpose is to:
+ * 1. Install the PoC app (`appTest`).
+ * 2. Run a single, designated test method in the device-side test class.
+ * 3. Clean up the app after the test is complete.
+ *
+ * All of the complex PoC logic is contained in the device-side test.
+ */
@RunWith(DeviceJUnit4ClassRunner.class)
public class HostsideTest extends NonRootSecurityTestCase {
@@ -34,15 +45,15 @@
static final String TEST_PKG = "com.android.security.appTest_AutoReproPlaceholder";
// The class name will be different from the application ID but will match the source code.
static final String TEST_CLASS = "com.android.security.DeviceTest";
+ // The single device-side test method that contains the entire PoC.
+ static final String TEST_METHOD = "testVulnerabilityFailsOnVulnerableDevice";
- /** An app test, which uses this host Java test to launch an Android instrumented test */
+ /**
+ * Installs the app and runs the single, self-contained device-side test.
+ */
@Test
- public void testWithApp() throws Exception {
- ITestDevice device = getDevice();
- assertTrue("could not disable root", device.disableAdbRoot());
- uninstallPackage(device, TEST_PKG);
-
+ public void testPoc() throws Exception {
installPackage(TEST_APP);
- runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+ runDeviceTests(TEST_PKG, TEST_CLASS, TEST_METHOD);
}
}