blob: ba8e7a1d3db329c9b797ff9bdd08994c6ea9002a [file] [log] [blame]
Joseph Wen6a34bb22015-02-25 14:00:39 -05001/*
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;
Christopher Tateeb87a4d2016-04-28 13:34:45 -070027import android.text.TextUtils;
Joseph Wen6a34bb22015-02-25 14:00:39 -050028import 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
Joseph Wen6a34bb22015-02-25 14:00:39 -050094 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
Christopher Tateeb87a4d2016-04-28 13:34:45 -0700105 ArrayList<String> finalHosts = new ArrayList<String>(hostList.length);
Joseph Wen6a34bb22015-02-25 14:00:39 -0500106 try {
107 ArrayList<String> sourceAssets = new ArrayList<String>();
108 for (String host : hostList) {
Christopher Tated268a222016-02-19 16:48:28 -0800109 // "*.example.tld" is validated via https://example.tld
110 if (host.startsWith("*.")) {
111 host = host.substring(2);
112 }
Joseph Wen6a34bb22015-02-25 14:00:39 -0500113 sourceAssets.add(createWebAssetString(scheme, host));
Christopher Tateeb87a4d2016-04-28 13:34:45 -0700114 finalHosts.add(host);
Joseph Wen6a34bb22015-02-25 14:00:39 -0500115 }
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
Christopher Tateeb87a4d2016-04-28 13:34:45 -0700135 // Required for CTS: log a few details of the validcation operation to be performed
136 logValidationParametersForCTS(verificationId, scheme, finalHosts, packageName);
137
Joseph Wen6a34bb22015-02-25 14:00:39 -0500138 serviceIntent.putExtras(extras);
139 context.startService(serviceIntent);
140 }
141 } else {
142 Log.w(TAG, "Intent action not supported: " + action);
143 }
144 }
145
Christopher Tateeb87a4d2016-04-28 13:34:45 -0700146 // 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
Joseph Wen6a34bb22015-02-25 14:00:39 -0500155 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}