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
17package com.android.statementservice;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.ResultReceiver;
27import android.text.TextUtils;
28import android.util.Log;
29import android.util.Patterns;
30
31import com.android.statementservice.retriever.Utils;
32
33import java.net.MalformedURLException;
34import java.net.URL;
35import java.util.ArrayList;
36import java.util.Collections;
37import java.util.List;
38import 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 */
53public 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