Add CreatorToken and CreatorTokenInfo

Add necessary data structure of platform to defend Intent Redirect attack
Bug: 363016360
Test: IntentTest
Flag: android.security.prevent_intent_redirect

Change-Id: I8b53a153e22fc7feecec66441d27749f71612e9e
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 031380d..044178c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -20,6 +20,7 @@
 import static android.content.ContentProvider.maybeAddUserId;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
 import static android.security.Flags.FLAG_FRP_ENFORCEMENT;
+import static android.security.Flags.preventIntentRedirect;
 
 import android.Manifest;
 import android.accessibilityservice.AccessibilityService;
@@ -7687,9 +7688,17 @@
     /** @hide */
     public static final int LOCAL_FLAG_FROM_SYSTEM = 1 << 5;
 
+    /**
+     * This flag indicates the creator token of this intent has been verified.
+     *
+     * @hide
+     */
+    public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
             EXTENDED_FLAG_FILTER_MISMATCH,
+            EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ExtendedFlags {}
@@ -7703,6 +7712,13 @@
     @TestApi
     public static final int EXTENDED_FLAG_FILTER_MISMATCH = 1 << 0;
 
+    /**
+     * This flag indicates the creator token of this intent is either missing or invalid.
+     *
+     * @hide
+     */
+    public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
+
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // toUri() and parseUri() options.
@@ -7870,6 +7886,7 @@
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
+        this.mCreatorTokenInfo = o.mCreatorTokenInfo;
 
         if (o.mCategories != null) {
             this.mCategories = new ArraySet<>(o.mCategories);
@@ -12176,6 +12193,60 @@
         return (mExtras != null) ? mExtras.describeContents() : 0;
     }
 
+    private static class CreatorTokenInfo {
+        // Stores a creator token for an intent embedded as an extra intent in a top level intent,
+        private IBinder mCreatorToken;
+        // Stores all extra keys whose values are intents for a top level intent.
+        private ArraySet<String> mExtraIntentKeys;
+    }
+
+    private @Nullable CreatorTokenInfo mCreatorTokenInfo;
+
+    /** @hide */
+    public void removeCreatorTokenInfo() {
+        mCreatorTokenInfo = null;
+    }
+
+    /** @hide */
+    public @Nullable IBinder getCreatorToken() {
+        return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mCreatorToken;
+    }
+
+    /** @hide */
+    public Set<String> getExtraIntentKeys() {
+        return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mExtraIntentKeys;
+    }
+
+    /** @hide */
+    public void setCreatorToken(@NonNull IBinder creatorToken) {
+        if (mCreatorTokenInfo == null) {
+            mCreatorTokenInfo = new CreatorTokenInfo();
+        }
+        mCreatorTokenInfo.mCreatorToken = creatorToken;
+    }
+
+    /**
+     * Collects keys in the extra bundle whose value are intents.
+     * @hide
+     */
+    public void collectExtraIntentKeys() {
+        if (!preventIntentRedirect()) return;
+
+        if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
+            for (String key : mExtras.keySet()) {
+                if (mExtras.get(key) instanceof Intent) {
+                    if (mCreatorTokenInfo == null) {
+                        mCreatorTokenInfo = new CreatorTokenInfo();
+                    }
+                    if (mCreatorTokenInfo.mExtraIntentKeys == null) {
+                        mCreatorTokenInfo.mExtraIntentKeys = new ArraySet<>();
+                    }
+                    mCreatorTokenInfo.mExtraIntentKeys.add(key);
+                }
+            }
+        }
+    }
+
     public void writeToParcel(Parcel out, int flags) {
         out.writeString8(mAction);
         Uri.writeToParcel(out, mData);
@@ -12225,6 +12296,16 @@
         } else {
             out.writeInt(0);
         }
+
+        if (preventIntentRedirect()) {
+            if (mCreatorTokenInfo == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                out.writeStrongBinder(mCreatorTokenInfo.mCreatorToken);
+                out.writeArraySet(mCreatorTokenInfo.mExtraIntentKeys);
+            }
+        }
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
@@ -12282,6 +12363,14 @@
         if (in.readInt() != 0) {
             mOriginalIntent = new Intent(in);
         }
+
+        if (preventIntentRedirect()) {
+            if (in.readInt() != 0) {
+                mCreatorTokenInfo = new CreatorTokenInfo();
+                mCreatorTokenInfo.mCreatorToken = in.readStrongBinder();
+                mCreatorTokenInfo.mExtraIntentKeys = (ArraySet<String>) in.readArraySet(null);
+            }
+        }
     }
 
     /**
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 56d3669..45e9def 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -52,3 +52,11 @@
     description: "Opt the system into enforcement of BAL"
     bug: "339403750"
 }
+
+flag {
+    name: "prevent_intent_redirect"
+    namespace: "responsible_apis"
+    description: "Prevent intent redirect attacks"
+    bug: "361143368"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d98836f..e9ce712 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -104,6 +104,7 @@
         "mockito-target-extended-minus-junit4",
         "TestParameterInjector",
         "android.content.res.flags-aconfig-java",
+        "android.security.flags-aconfig-java",
     ],
 
     libs: [
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
new file mode 100644
index 0000000..d169ce3
--- /dev/null
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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 android.content;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.security.Flags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ *  Build/Install/Run:
+ *   atest FrameworksCoreTests:IntentTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IntentTest {
+    private static final String TEST_ACTION = "android.content.IntentTest_test";
+    private static final String TEST_EXTRA_NAME = "testExtraName";
+    private static final Uri TEST_URI = Uri.parse("content://com.example/people");
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+    public void testReadFromParcelWithExtraIntentKeys() {
+        Intent intent = new Intent("TEST_ACTION");
+        intent.putExtra(TEST_EXTRA_NAME, new Intent(TEST_ACTION));
+        intent.putExtra(TEST_EXTRA_NAME + "2", 1);
+
+        intent.collectExtraIntentKeys();
+        final Parcel parcel = Parcel.obtain();
+        intent.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final Intent target = new Intent();
+        target.readFromParcel(parcel);
+
+        assertEquals(intent.getAction(), target.getAction());
+        assertEquals(intent.getExtraIntentKeys(), target.getExtraIntentKeys());
+        assertThat(intent.getExtraIntentKeys()).hasSize(1);
+    }
+
+    @Test
+    public void testCreatorTokenInfo() {
+        Intent intent = new Intent(TEST_ACTION);
+        IBinder creatorToken = new Binder();
+
+        intent.setCreatorToken(creatorToken);
+        assertThat(intent.getCreatorToken()).isEqualTo(creatorToken);
+
+        intent.removeCreatorTokenInfo();
+        assertThat(intent.getCreatorToken()).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+    public void testCollectExtraIntentKeys() {
+        Intent intent = new Intent(TEST_ACTION);
+        Intent extraIntent = new Intent(TEST_ACTION, TEST_URI);
+        intent.putExtra(TEST_EXTRA_NAME, extraIntent);
+
+        intent.collectExtraIntentKeys();
+
+        assertThat(intent.getExtraIntentKeys()).hasSize(1);
+        assertThat(intent.getExtraIntentKeys()).contains(TEST_EXTRA_NAME);
+    }
+
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3c57476..7ff6832 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -262,6 +262,7 @@
 import android.content.AttributionSource;
 import android.content.AutofillOptions;
 import android.content.BroadcastReceiver;
+import android.content.ClipData;
 import android.content.ComponentCallbacks2;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
@@ -418,7 +419,6 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.MemInfoReader;
 import com.android.internal.util.Preconditions;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.BootReceiver;
 import com.android.server.DeviceIdleInternal;
@@ -438,6 +438,7 @@
 import com.android.server.appop.AppOpsService;
 import com.android.server.compat.PlatformCompat;
 import com.android.server.contentcapture.ContentCaptureManagerInternal;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
 import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.firewall.IntentFirewall;
 import com.android.server.graphics.fonts.FontManagerInternal;
@@ -482,6 +483,7 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
@@ -499,6 +501,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -19078,4 +19081,87 @@
     Freezer getFreezer() {
         return mFreezer;
     }
+
+    // Set of IntentCreatorToken objects that are currently active.
+    private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>>
+            sIntentCreatorTokenCache = new ConcurrentHashMap<>();
+
+    /**
+     * A binder token used to keep track of which app created the intent. This token can be used to
+     * defend against intent redirect attacks. It stores uid of the intent creator and key fields of
+     * the intent to make it impossible for attacker to fake uid with a malicious intent.
+     *
+     * @hide
+     */
+    public static final class IntentCreatorToken extends Binder {
+        @NonNull
+        private final Key mKeyFields;
+
+        public IntentCreatorToken(int creatorUid, Intent intent) {
+            super();
+            this.mKeyFields = new Key(creatorUid, intent);
+        }
+
+        public int getCreatorUid() {
+            return mKeyFields.mCreatorUid;
+        }
+
+        /** {@hide} */
+        public static boolean isValid(@NonNull Intent intent) {
+            IBinder binder = intent.getCreatorToken();
+            IntentCreatorToken token = null;
+            if (binder instanceof IntentCreatorToken) {
+                token = (IntentCreatorToken) binder;
+            }
+            return token != null && token.mKeyFields.equals(
+                    new Key(token.mKeyFields.mCreatorUid, intent));
+        }
+
+        private static class Key {
+            private Key(int creatorUid, Intent intent) {
+                this.mCreatorUid = creatorUid;
+                this.mAction = intent.getAction();
+                this.mData = intent.getData();
+                this.mType = intent.getType();
+                this.mPackage = intent.getPackage();
+                this.mComponent = intent.getComponent();
+                this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS;
+                ClipData clipData = intent.getClipData();
+                if (clipData != null) {
+                    this.mClipDataUris = new ArrayList<>(clipData.getItemCount());
+                    for (int i = 0; i < clipData.getItemCount(); i++) {
+                        this.mClipDataUris.add(clipData.getItemAt(i).getUri());
+                    }
+                }
+            }
+
+            private final int mCreatorUid;
+            private final String mAction;
+            private final Uri mData;
+            private final String mType;
+            private final String mPackage;
+            private final ComponentName mComponent;
+            private final int mFlags;
+            private List<Uri> mClipDataUris;
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                Key key = (Key) o;
+                return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals(
+                        mAction, key.mAction) && Objects.equals(mData, key.mData)
+                        && Objects.equals(mType, key.mType) && Objects.equals(mPackage,
+                        key.mPackage) && Objects.equals(mComponent, key.mComponent)
+                        && Objects.equals(mClipDataUris, key.mClipDataUris);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
+                        mFlags,
+                        mClipDataUris);
+            }
+        }
+    }
 }