Home | History | Annotate | Download | only in statementservice
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.statementservice;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.pm.PackageManager;
     23 import android.content.pm.PackageManager.NameNotFoundException;
     24 import android.os.Bundle;
     25 import android.os.Handler;
     26 import android.os.ResultReceiver;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.util.Patterns;
     30 
     31 import com.android.statementservice.retriever.Utils;
     32 
     33 import java.net.MalformedURLException;
     34 import java.net.URL;
     35 import java.util.ArrayList;
     36 import java.util.Collections;
     37 import java.util.List;
     38 import java.util.regex.Pattern;
     39 
     40 /**
     41  * Receives {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION} broadcast and calls
     42  * {@link DirectStatementService} to verify the request. Calls
     43  * {@link PackageManager#verifyIntentFilter} to notify {@link PackageManager} the result of the
     44  * verification.
     45  *
     46  * This implementation of the API will send a HTTP request for each host specified in the query.
     47  * To avoid overwhelming the network at app install time, {@code MAX_HOSTS_PER_REQUEST} limits
     48  * the maximum number of hosts in a query. If a query contains more than
     49  * {@code MAX_HOSTS_PER_REQUEST} hosts, it will fail immediately without making any HTTP request
     50  * and call {@link PackageManager#verifyIntentFilter} with
     51  * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}.
     52  */
     53 public final class IntentFilterVerificationReceiver extends BroadcastReceiver {
     54     private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName();
     55 
     56     private static final Integer MAX_HOSTS_PER_REQUEST = 10;
     57 
     58     private static final String HANDLE_ALL_URLS_RELATION
     59             = "delegate_permission/common.handle_all_urls";
     60 
     61     private static final String ANDROID_ASSET_FORMAT = "{\"namespace\": \"android_app\", "
     62             + "\"package_name\": \"%s\", \"sha256_cert_fingerprints\": [\"%s\"]}";
     63     private static final String WEB_ASSET_FORMAT = "{\"namespace\": \"web\", \"site\": \"%s\"}";
     64     private static final Pattern ANDROID_PACKAGE_NAME_PATTERN =
     65             Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$");
     66     private static final String TOO_MANY_HOSTS_FORMAT =
     67             "Request contains %d hosts which is more than the allowed %d.";
     68 
     69     private static void sendErrorToPackageManager(PackageManager packageManager,
     70             int verificationId) {
     71         packageManager.verifyIntentFilter(verificationId,
     72                 PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
     73                 Collections.<String>emptyList());
     74     }
     75 
     76     @Override
     77     public void onReceive(Context context, Intent intent) {
     78         final String action = intent.getAction();
     79         if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) {
     80             Bundle inputExtras = intent.getExtras();
     81             if (inputExtras != null) {
     82                 Intent serviceIntent = new Intent(context, DirectStatementService.class);
     83                 serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION);
     84 
     85                 int verificationId = inputExtras.getInt(
     86                         PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID);
     87                 String scheme = inputExtras.getString(
     88                         PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME);
     89                 String hosts = inputExtras.getString(
     90                         PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS);
     91                 String packageName = inputExtras.getString(
     92                         PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME);
     93 
     94                 Bundle extras = new Bundle();
     95                 extras.putString(DirectStatementService.EXTRA_RELATION, HANDLE_ALL_URLS_RELATION);
     96 
     97                 String[] hostList = hosts.split(" ");
     98                 if (hostList.length > MAX_HOSTS_PER_REQUEST) {
     99                     Log.w(TAG, String.format(TOO_MANY_HOSTS_FORMAT,
    100                             hostList.length, MAX_HOSTS_PER_REQUEST));
    101                     sendErrorToPackageManager(context.getPackageManager(), verificationId);
    102                     return;
    103                 }
    104 
    105                 ArrayList<String> finalHosts = new ArrayList<String>(hostList.length);
    106                 try {
    107                     ArrayList<String> sourceAssets = new ArrayList<String>();
    108                     for (String host : hostList) {
    109                         // "*.example.tld" is validated via https://example.tld
    110                         if (host.startsWith("*.")) {
    111                             host = host.substring(2);
    112                         }
    113                         sourceAssets.add(createWebAssetString(scheme, host));
    114                         finalHosts.add(host);
    115                     }
    116                     extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
    117                             sourceAssets);
    118                 } catch (MalformedURLException e) {
    119                     Log.w(TAG, "Error when processing input host: " + e.getMessage());
    120                     sendErrorToPackageManager(context.getPackageManager(), verificationId);
    121                     return;
    122                 }
    123                 try {
    124                     extras.putString(DirectStatementService.EXTRA_TARGET_ASSET_DESCRIPTOR,
    125                             createAndroidAssetString(context, packageName));
    126                 } catch (NameNotFoundException e) {
    127                     Log.w(TAG, "Error when processing input Android package: " + e.getMessage());
    128                     sendErrorToPackageManager(context.getPackageManager(), verificationId);
    129                     return;
    130                 }
    131                 extras.putParcelable(DirectStatementService.EXTRA_RESULT_RECEIVER,
    132                         new IsAssociatedResultReceiver(
    133                                 new Handler(), context.getPackageManager(), verificationId));
    134 
    135                 // Required for CTS: log a few details of the validcation operation to be performed
    136                 logValidationParametersForCTS(verificationId, scheme, finalHosts, packageName);
    137 
    138                 serviceIntent.putExtras(extras);
    139                 context.startService(serviceIntent);
    140             }
    141         } else {
    142             Log.w(TAG, "Intent action not supported: " + action);
    143         }
    144     }
    145 
    146     // CTS requirement: logging of the validation parameters in a specific format
    147     private static final String CTS_LOG_FORMAT =
    148             "Verifying IntentFilter. verificationId:%d scheme:\"%s\" hosts:\"%s\" package:\"%s\".";
    149     private void logValidationParametersForCTS(int verificationId, String scheme,
    150             ArrayList<String> finalHosts, String packageName) {
    151         String hostString = TextUtils.join(" ", finalHosts.toArray());
    152         Log.i(TAG, String.format(CTS_LOG_FORMAT, verificationId, scheme, hostString, packageName));
    153     }
    154 
    155     private String createAndroidAssetString(Context context, String packageName)
    156             throws NameNotFoundException {
    157         if (!ANDROID_PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
    158             throw new NameNotFoundException("Input package name is not valid.");
    159         }
    160 
    161         List<String> certFingerprints =
    162                 Utils.getCertFingerprintsFromPackageManager(packageName, context);
    163 
    164         return String.format(ANDROID_ASSET_FORMAT, packageName,
    165                 Utils.joinStrings("\", \"", certFingerprints));
    166     }
    167 
    168     private String createWebAssetString(String scheme, String host) throws MalformedURLException {
    169         if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
    170             throw new MalformedURLException("Input host is not valid.");
    171         }
    172         if (!scheme.equals("http") && !scheme.equals("https")) {
    173             throw new MalformedURLException("Input scheme is not valid.");
    174         }
    175 
    176         return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
    177     }
    178 
    179     /**
    180      * Receives the result of {@code StatementService.CHECK_ACTION} from
    181      * {@link DirectStatementService} and passes it back to {@link PackageManager}.
    182      */
    183     private static class IsAssociatedResultReceiver extends ResultReceiver {
    184 
    185         private final int mVerificationId;
    186         private final PackageManager mPackageManager;
    187 
    188         public IsAssociatedResultReceiver(Handler handler, PackageManager packageManager,
    189                 int verificationId) {
    190             super(handler);
    191             mVerificationId = verificationId;
    192             mPackageManager = packageManager;
    193         }
    194 
    195         @Override
    196         protected void onReceiveResult(int resultCode, Bundle resultData) {
    197             if (resultCode == DirectStatementService.RESULT_SUCCESS) {
    198                 if (resultData.getBoolean(DirectStatementService.IS_ASSOCIATED)) {
    199                     mPackageManager.verifyIntentFilter(mVerificationId,
    200                             PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS,
    201                             Collections.<String>emptyList());
    202                 } else {
    203                     mPackageManager.verifyIntentFilter(mVerificationId,
    204                             PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
    205                             resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES));
    206                 }
    207             } else {
    208                 sendErrorToPackageManager(mPackageManager, mVerificationId);
    209             }
    210         }
    211     }
    212 }
    213