Introduction

What is a WebAPK?

When the user adds a Progressive Web App to their home screen on Android, Chrome automatically generates an APK for them, which is called a WebAPK. Being installed via an APK makes it possible for the app to show up in the app launcher, in Android’s app settings and to register a set of intent filters.

To generate the WebAPK, Chrome looks at the web app manifest and other metadata.

When the WebAPK is installed on the phone, it will register a set of intent filters for all URLs within the scope of the app. When a user clicks on a link that is within the scope of the app, the app will be opened, rather than opening within a browser tab.

The WebAPK runs as a Trusted Web Activity.

Intent

From a simple click in the browser, to having an entire APK installed directly in the background, WebAPK seems to be a complex web of several different Android components. The intent of this blogpost is to understand the entire flow from the user tapping on the “Install App” button, to when the APK is installed on the phone.

Chrome version: 117.0.5938.153 Play Store version: 37.9.18-29 Android version: MIUI Global 12.0.1.2 on Redmi 9

Understanding the structure of the code

It is anyone’s guess that the magic first begins inside the Chrome browser Android application, which is powered by the Chromium browser laid over a Java application. The Chromium browser is thankfully open-source, and with it some of the WebAPK-related Android functionality too. This source code is available at https://source.chromium.org/chromium/chromium/src/+/f97ec5363b5a8666ac40285f0d4bfc23bdc9f87e:components/webapps/browser/android/. For other Chrome-specific functionality, we will need to decompile the Chrome APK from an Android phone. As you will see later on in the blogpost, we will also need to decompile the Play Store application later in order to understand the complete flow.

“Add to Home Screen”

When a user clicks on “Add to Home Screen” or “Install App” button as shown below, we reach the code residing in the AddToHomeScreenDialogView.java

Here we can see that a call to mDelegate.onAddToHomescreen(); is made:

This calls into the following code in AddToHomescreenMediator.java, which seems to be some call to native code based on the “JNI” in the function name. The Java Native Interface is an interface programming framework that enables Java code running in a Java virtual machine to call and be called by native applications and libraries written in other languages such as C, C++ and assembly.

We look for the corresponding code and land in components/webapps/browser/android/add_to_homescreen_mediator.cc, with the following code:

Here we can see that the code checks the params object to figure out if the requested installation is for a WebAPK or not. The code for this struct can be found at components/webapps/browser/android/add_to_homescreen_params.h.

Following the call into AddToHomescreenInstaller::Install(GetWebContents(), *params_, event_callback_), where GetWebContents() returns the HTML document of the current web page loading inside the browser, we reach the following code:

Here we can see that if it’s a WebAPK installation, it will call WebappsClient::Get()->InstallWebApk(web_contents, params);. Let’s look at the code for that:

The shortcut_info structure contains information like the manifest URL of the app to be installed. Going further, we reach:

Going a few calls deeper, we finally reach the following:

CheckFreeSpace() calls back to the Java code as follows:

void WebApkInstaller::CheckFreeSpace() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_WebApkInstaller_checkFreeSpace(env, java_ref_);
}

The code inside the Android app simply checks for free space, and proceeds to call back to the C code:

As we can see, it serializes information for the WebAPK into a protobuf stream, and sends a request to the Google minting server, which is responsible for generating the WebAPK, as we will see further.

The protobuf definition for a WebAPK request can be found at /components/webapk/webapk.proto

Generation of the WebAPK - Google’s minting server

Simply intercepting the traffic while this process reveals the following request is sent to Google’s servers. We can see a protobuf serialized message being sent, and in response getting another protobuf response containing details of the generated WebAPK.

Parsing the WebApkResponse

The OnUrlLoaderComplete() eventually calls into InstallOrUpdateWebApk():

Let’s talk about this code. The function takes a “package name” and some kind of “token”.

Digging into the source code for OnUrlLoaderComplete(), we see the following:

The following code takes the response from the Google minting server discussed earlier, and parses it into a WebApkResponse:

std::unique_ptr<webapk::WebApkResponse> response(new webapk::WebApkResponse);
  if (!response_body || !response->ParseFromString(*response_body)) {
    LOG(WARNING) << "WebAPK server did not return proto.";
    OnResult(webapps::WebApkInstallResult::SERVER_ERROR);
    return;
  }

The protobuf definition for a WebApkResponse can be found at out/android-Debug/gen/components/webapk/webapk.pb.h. We can see that at field 1 it contains the package_name:

// optional string package_name = 1;

and that at field 6 it contains a token:

// optional string token = 6;

These are the fields that are used to call InstallOrUpdateWebApk(response->package_name(), token);, which can be seen in the image, once again, the fields corresponding to 1 and 6:

Finally, the native code calls Java_WebApkInstaller_installWebApkAsync(env, java_ref_, java_webapk_package, webapk_version_, java_title, java_token, source_, java_primary_icon);. This leads us back to the Java code inside the Android app,

After this, I was unable to find the definition of installAsync(packageName, version, title, token, callback); in the Chromium project, which is when I decided to decompile the Chrome application itself. This is probably because every browser has it’s own custom implementation of this method inside the GooglePlayWebApkInstallDelegate interface which can be found at chrome/android/java/src/org/chromium/chrome/browser/webapps/GooglePlayWebApkInstallDelegate.java.

Chrome talks to Google Play services

I was able to quickly track down the same package and class file in org.chromium.chrome.browser.webapps.WebApkInstaller, this time with a definition of the installAsync method. This is what I found:

Note that most of the code at this point was initially heavily obfuscated, and variables and functions were renamed after hours of analyzing it.

We see that a service connection is used to interact with a bound service at com.android.vending (which is the package name for Play Store) and the intent with action com.google.android.finsky.BIND_PLAY_INSTALL_SERVICE. Looking into this service connection, we see the following code:

Looking more into the w71 binder object:

At line 30 we see that it transacts with the com.google.android.finsky.installapi.IPlayInstallService binder the service connection is bound to. It sends the current package name, package name and the token to the binder transaction. At this point, we need to turn to the Play Store package to see how it handles this transaction. You can find more abount binder transactions here.

After decompiling the Play Store package and opening com.google.android.finsky.installapi.PlayInstallService we see the following:

Going through the code for the installBinder binder (real class name qeu), we see the following call being made:

Bundle mo10293a = mo10293a(parcel.readString(), parcel.readString(), (Bundle) parcelOperator.createParcel(parcel, Bundle.CREATOR));

This is basically inside the onTransaction call for this binder, where the sending package name, the installation package name and bundle containing our token is taken from the transaction. Looking into the code for mo10293a() we will see the following function call with the same bundle and package name combo:

((qfa) arrayList.get(i)).mo10213a(uryVar);

There is also a check early on in the function:

if (!this.f136102c.checkUid(packageName)) {
            return m10294b(-1);
        }

This check ensures that the sending package name in the transaction call matches the Uid of the package that the binder is transacting with.

The code for the mo10213a() function can be found in a class called qfp:

Following line 14 we come across a whole bunch of checks on the transaction information, which are needed to pass in order for the package to finally be installed. All the checks look quite simple, except for the first one, which is to do with signatures. Going down a little deeper, we are faced with a whole class that deals with Google package signature verification (again, variable and function naming done independently):

As you can see, the package communicates with a service called com.google.android.gms.common.internal.IGoogleCertificatesApi to check if the signature of the calling package is a Google verified signature or not.

After these checks, the WebAPK installation begins. But as far as security goes, this is the last check in the process, after which Play Store retrieves the APK using the token sent earlier in the transaction.