From 374e6d6cc9b587a195c49003e84a5a2543477962 Mon Sep 17 00:00:00 2001 From: Winson Date: Thu, 17 Dec 2020 14:18:41 -0800 Subject: [PATCH] Update com.android.statementservice to support v2 API Brings AOSP up to a true representative implementation which can be shipped on production devices. Bug: 171219506 Test: manual, push AOSP, pm enable, pm verify-app-links Change-Id: I5de6405afe884a19d35d09b266457c4ad4eee91b --- data/etc/privapp-permissions-platform.xml | 2 + packages/StatementService/Android.bp | 19 +- packages/StatementService/AndroidManifest.xml | 65 ++-- .../DirectStatementService.java | 293 ------------------ .../ExceptionLoggingFutureTask.java | 46 --- .../IntentFilterVerificationReceiver.java | 212 ------------- .../StatementServiceApplication.kt | 35 +++ .../domain/BaseDomainVerificationReceiver.kt | 41 +++ .../domain/BootCompletedReceiver.kt | 59 ++++ .../domain/DomainVerificationReceiverV1.kt | 79 +++++ .../domain/DomainVerificationReceiverV2.kt | 116 +++++++ .../domain/DomainVerificationUtils.kt | 77 +++++ .../statementservice/domain/DomainVerifier.kt | 146 +++++++++ .../statementservice/domain/VerifyStatus.kt | 66 ++++ .../domain/worker/BaseRequestWorker.kt | 34 ++ .../domain/worker/CollectV1Worker.kt | 89 ++++++ .../domain/worker/RetryRequestWorker.kt | 79 +++++ .../domain/worker/SingleV1RequestWorker.kt | 87 ++++++ .../domain/worker/SingleV2RequestWorker.kt | 68 ++++ .../network/retriever/StatementParser.kt | 109 +++++++ .../network/retriever/StatementRetriever.kt | 121 ++++++++ .../network/retriever/UrlFetcher.kt | 86 +++++ .../retriever/AbstractAsset.java | 3 + .../retriever/AbstractStatementRetriever.java | 108 ------- .../retriever/AndroidAppAsset.java | 26 +- .../retriever/AndroidAppAssetMatcher.java | 2 +- .../retriever/AndroidPackageInfoFetcher.java | 93 ------ .../retriever/AssetFactory.java | 12 +- .../retriever/AssetMatcherFactory.java | 10 +- .../retriever/DirectStatementRetriever.java | 213 ------------- .../retriever/ParsedStatement.java | 41 --- .../statementservice/retriever/Statement.java | 11 +- .../retriever/StatementParser.java | 111 ------- .../retriever/URLFetcher.java | 197 ------------ .../statementservice/retriever/Utils.java | 159 ---------- .../statementservice/retriever/WebAsset.java | 14 +- .../retriever/WebContent.java | 8 +- .../statementservice/utils/AndroidUtils.kt | 63 ++++ .../android/statementservice/utils/Result.kt | 33 ++ .../statementservice/utils/StatementUtils.kt | 162 ++++++++++ .../server/pm/PackageManagerService.java | 38 ++- 41 files changed, 1700 insertions(+), 1533 deletions(-) delete mode 100644 packages/StatementService/src/com/android/statementservice/DirectStatementService.java delete mode 100644 packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java delete mode 100644 packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java create mode 100644 packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt create mode 100644 packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt create mode 100644 packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt create mode 100644 packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt create mode 100644 packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java delete mode 100644 packages/StatementService/src/com/android/statementservice/retriever/Utils.java create mode 100644 packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt create mode 100644 packages/StatementService/src/com/android/statementservice/utils/Result.kt create mode 100644 packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 8d4739dbc255..30f48b95b890 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -498,6 +498,8 @@ applications that come with the platform + + diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp index 32defc822733..a0d8ac9b8adc 100644 --- a/packages/StatementService/Android.bp +++ b/packages/StatementService/Android.bp @@ -22,17 +22,24 @@ package { android_app { name: "StatementService", - defaults: ["platform_app_defaults"], - srcs: ["src/**/*.java"], + // Removed because Errorprone doesn't work with Kotlin, can fix up in the future + // defaults: ["platform_app_defaults"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], optimize: { proguard_flags_files: ["proguard.flags"], }, + target_sdk_version: "29", platform_apis: true, privileged: true, - libs: ["org.apache.http.legacy"], - uses_libs: ["org.apache.http.legacy"], + certificate: "platform", static_libs: [ - "libprotobuf-java-nano", - "volley", + "androidx.appcompat_appcompat", + "androidx.collection_collection-ktx", + "androidx.work_work-runtime", + "androidx.work_work-runtime-ktx", + "kotlinx-coroutines-android", ], } diff --git a/packages/StatementService/AndroidManifest.xml b/packages/StatementService/AndroidManifest.xml index e0abd50b9de9..42cd14314954 100644 --- a/packages/StatementService/AndroidManifest.xml +++ b/packages/StatementService/AndroidManifest.xml @@ -14,41 +14,62 @@ limitations under the License. --> - + - + + + + + - - - - - - - + android:label="@string/service_name" + android:allowBackup="false" + android:name=".StatementServiceApplication" + > - - + android:name=".domain.BootCompletedReceiver" + android:exported="true"> + + + + + + + + + + + + + diff --git a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java deleted file mode 100644 index 659696e0e212..000000000000 --- a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice; - -import android.app.Service; -import android.content.Intent; -import android.net.http.HttpResponseCache; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.ResultReceiver; -import android.util.Log; - -import com.android.statementservice.retriever.AbstractAsset; -import com.android.statementservice.retriever.AbstractAssetMatcher; -import com.android.statementservice.retriever.AbstractStatementRetriever; -import com.android.statementservice.retriever.AbstractStatementRetriever.Result; -import com.android.statementservice.retriever.AssociationServiceException; -import com.android.statementservice.retriever.Relation; -import com.android.statementservice.retriever.Statement; - -import org.json.JSONException; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; - -/** - * Handles com.android.statementservice.service.CHECK_ALL_ACTION intents. - */ -public final class DirectStatementService extends Service { - private static final String TAG = DirectStatementService.class.getSimpleName(); - - /** - * Returns true if every asset in {@code SOURCE_ASSET_DESCRIPTORS} is associated with {@code - * EXTRA_TARGET_ASSET_DESCRIPTOR} for {@code EXTRA_RELATION} relation. - * - *

Takes parameter {@code EXTRA_RELATION}, {@code SOURCE_ASSET_DESCRIPTORS}, {@code - * EXTRA_TARGET_ASSET_DESCRIPTOR}, and {@code EXTRA_RESULT_RECEIVER}. - */ - public static final String CHECK_ALL_ACTION = - "com.android.statementservice.service.CHECK_ALL_ACTION"; - - /** - * Parameter for {@link #CHECK_ALL_ACTION}. - * - *

A relation string. - */ - public static final String EXTRA_RELATION = - "com.android.statementservice.service.RELATION"; - - /** - * Parameter for {@link #CHECK_ALL_ACTION}. - * - *

An array of asset descriptors in JSON. - */ - public static final String EXTRA_SOURCE_ASSET_DESCRIPTORS = - "com.android.statementservice.service.SOURCE_ASSET_DESCRIPTORS"; - - /** - * Parameter for {@link #CHECK_ALL_ACTION}. - * - *

An asset descriptor in JSON. - */ - public static final String EXTRA_TARGET_ASSET_DESCRIPTOR = - "com.android.statementservice.service.TARGET_ASSET_DESCRIPTOR"; - - /** - * Parameter for {@link #CHECK_ALL_ACTION}. - * - *

A {@code ResultReceiver} instance that will be used to return the result. If the request - * failed, return {@link #RESULT_FAIL} and an empty {@link android.os.Bundle}. Otherwise, return - * {@link #RESULT_SUCCESS} and a {@link android.os.Bundle} with the result stored in {@link - * #IS_ASSOCIATED}. - */ - public static final String EXTRA_RESULT_RECEIVER = - "com.android.statementservice.service.RESULT_RECEIVER"; - - /** - * A boolean bundle entry that stores the result of {@link #CHECK_ALL_ACTION}. - * This is set only if the service returns with {@code RESULT_SUCCESS}. - * {@code IS_ASSOCIATED} is true if and only if {@code FAILED_SOURCES} is empty. - */ - public static final String IS_ASSOCIATED = "is_associated"; - - /** - * A String ArrayList bundle entry that stores sources that can't be verified. - */ - public static final String FAILED_SOURCES = "failed_sources"; - - /** - * Returned by the service if the request is successfully processed. The caller should check - * the {@code IS_ASSOCIATED} field to determine if the association exists or not. - */ - public static final int RESULT_SUCCESS = 0; - - /** - * Returned by the service if the request failed. The request will fail if, for example, the - * input is not well formed, or the network is not available. - */ - public static final int RESULT_FAIL = 1; - - private static final long HTTP_CACHE_SIZE_IN_BYTES = 1 * 1024 * 1024; // 1 MBytes - private static final String CACHE_FILENAME = "request_cache"; - - private AbstractStatementRetriever mStatementRetriever; - private Handler mHandler; - private HandlerThread mThread; - private HttpResponseCache mHttpResponseCache; - - @Override - public void onCreate() { - mThread = new HandlerThread("DirectStatementService thread", - android.os.Process.THREAD_PRIORITY_BACKGROUND); - mThread.start(); - onCreate(AbstractStatementRetriever.createDirectRetriever(this), mThread.getLooper(), - getCacheDir()); - } - - /** - * Creates a DirectStatementService with the dependencies passed in for easy testing. - */ - public void onCreate(AbstractStatementRetriever statementRetriever, Looper looper, - File cacheDir) { - super.onCreate(); - mStatementRetriever = statementRetriever; - mHandler = new Handler(looper); - - try { - File httpCacheDir = new File(cacheDir, CACHE_FILENAME); - mHttpResponseCache = HttpResponseCache.install(httpCacheDir, HTTP_CACHE_SIZE_IN_BYTES); - } catch (IOException e) { - Log.i(TAG, "HTTPS response cache installation failed:" + e); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - final HttpResponseCache responseCache = mHttpResponseCache; - mHandler.post(new Runnable() { - public void run() { - try { - if (responseCache != null) { - responseCache.delete(); - } - } catch (IOException e) { - Log.i(TAG, "HTTP(S) response cache deletion failed:" + e); - } - Looper.myLooper().quit(); - } - }); - mHttpResponseCache = null; - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (intent == null) { - Log.e(TAG, "onStartCommand called with null intent"); - return START_STICKY; - } - - if (intent.getAction().equals(CHECK_ALL_ACTION)) { - - Bundle extras = intent.getExtras(); - List sources = extras.getStringArrayList(EXTRA_SOURCE_ASSET_DESCRIPTORS); - String target = extras.getString(EXTRA_TARGET_ASSET_DESCRIPTOR); - String relation = extras.getString(EXTRA_RELATION); - ResultReceiver resultReceiver = extras.getParcelable(EXTRA_RESULT_RECEIVER); - - if (resultReceiver == null) { - Log.e(TAG, " Intent does not have extra " + EXTRA_RESULT_RECEIVER); - return START_STICKY; - } - if (sources == null) { - Log.e(TAG, " Intent does not have extra " + EXTRA_SOURCE_ASSET_DESCRIPTORS); - resultReceiver.send(RESULT_FAIL, Bundle.EMPTY); - return START_STICKY; - } - if (target == null) { - Log.e(TAG, " Intent does not have extra " + EXTRA_TARGET_ASSET_DESCRIPTOR); - resultReceiver.send(RESULT_FAIL, Bundle.EMPTY); - return START_STICKY; - } - if (relation == null) { - Log.e(TAG, " Intent does not have extra " + EXTRA_RELATION); - resultReceiver.send(RESULT_FAIL, Bundle.EMPTY); - return START_STICKY; - } - - mHandler.post(new ExceptionLoggingFutureTask( - new IsAssociatedCallable(sources, target, relation, resultReceiver), TAG)); - } else { - Log.e(TAG, "onStartCommand called with unsupported action: " + intent.getAction()); - } - return START_STICKY; - } - - private class IsAssociatedCallable implements Callable { - - private List mSources; - private String mTarget; - private String mRelation; - private ResultReceiver mResultReceiver; - - public IsAssociatedCallable(List sources, String target, String relation, - ResultReceiver resultReceiver) { - mSources = sources; - mTarget = target; - mRelation = relation; - mResultReceiver = resultReceiver; - } - - private boolean verifyOneSource(AbstractAsset source, AbstractAssetMatcher target, - Relation relation) throws AssociationServiceException { - Result statements = mStatementRetriever.retrieveStatements(source); - for (Statement statement : statements.getStatements()) { - if (relation.matches(statement.getRelation()) - && target.matches(statement.getTarget())) { - return true; - } - } - return false; - } - - @Override - public Void call() { - Bundle result = new Bundle(); - ArrayList failedSources = new ArrayList(); - AbstractAssetMatcher target; - Relation relation; - try { - target = AbstractAssetMatcher.createMatcher(mTarget); - relation = Relation.create(mRelation); - } catch (AssociationServiceException | JSONException e) { - Log.e(TAG, "isAssociatedCallable failed with exception", e); - mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY); - return null; - } - - boolean allSourcesVerified = true; - for (String sourceString : mSources) { - AbstractAsset source; - try { - source = AbstractAsset.create(sourceString); - } catch (AssociationServiceException e) { - mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY); - return null; - } - - try { - if (!verifyOneSource(source, target, relation)) { - failedSources.add(source.toJson()); - allSourcesVerified = false; - } - } catch (AssociationServiceException e) { - failedSources.add(source.toJson()); - allSourcesVerified = false; - } - } - - result.putBoolean(IS_ASSOCIATED, allSourcesVerified); - result.putStringArrayList(FAILED_SOURCES, failedSources); - mResultReceiver.send(RESULT_SUCCESS, result); - return null; - } - } -} diff --git a/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java b/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java deleted file mode 100644 index 20c7f9715895..000000000000 --- a/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice; - -import android.util.Log; - -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; - -/** - * {@link FutureTask} that logs unhandled exceptions. - */ -final class ExceptionLoggingFutureTask extends FutureTask { - - private final String mTag; - - public ExceptionLoggingFutureTask(Callable callable, String tag) { - super(callable); - mTag = tag; - } - - @Override - protected void done() { - try { - get(); - } catch (ExecutionException | InterruptedException e) { - Log.e(mTag, "Uncaught exception.", e); - throw new RuntimeException(e); - } - } -} diff --git a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java deleted file mode 100644 index ba8e7a1d3db3..000000000000 --- a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; -import android.text.TextUtils; -import android.util.Log; -import android.util.Patterns; - -import com.android.statementservice.retriever.Utils; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Receives {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION} broadcast and calls - * {@link DirectStatementService} to verify the request. Calls - * {@link PackageManager#verifyIntentFilter} to notify {@link PackageManager} the result of the - * verification. - * - * This implementation of the API will send a HTTP request for each host specified in the query. - * To avoid overwhelming the network at app install time, {@code MAX_HOSTS_PER_REQUEST} limits - * the maximum number of hosts in a query. If a query contains more than - * {@code MAX_HOSTS_PER_REQUEST} hosts, it will fail immediately without making any HTTP request - * and call {@link PackageManager#verifyIntentFilter} with - * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}. - */ -public final class IntentFilterVerificationReceiver extends BroadcastReceiver { - private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName(); - - private static final Integer MAX_HOSTS_PER_REQUEST = 10; - - private static final String HANDLE_ALL_URLS_RELATION - = "delegate_permission/common.handle_all_urls"; - - private static final String ANDROID_ASSET_FORMAT = "{\"namespace\": \"android_app\", " - + "\"package_name\": \"%s\", \"sha256_cert_fingerprints\": [\"%s\"]}"; - private static final String WEB_ASSET_FORMAT = "{\"namespace\": \"web\", \"site\": \"%s\"}"; - private static final Pattern ANDROID_PACKAGE_NAME_PATTERN = - Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$"); - private static final String TOO_MANY_HOSTS_FORMAT = - "Request contains %d hosts which is more than the allowed %d."; - - private static void sendErrorToPackageManager(PackageManager packageManager, - int verificationId) { - packageManager.verifyIntentFilter(verificationId, - PackageManager.INTENT_FILTER_VERIFICATION_FAILURE, - Collections.emptyList()); - } - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) { - Bundle inputExtras = intent.getExtras(); - if (inputExtras != null) { - Intent serviceIntent = new Intent(context, DirectStatementService.class); - serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION); - - int verificationId = inputExtras.getInt( - PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID); - String scheme = inputExtras.getString( - PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME); - String hosts = inputExtras.getString( - PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS); - String packageName = inputExtras.getString( - PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME); - - Bundle extras = new Bundle(); - extras.putString(DirectStatementService.EXTRA_RELATION, HANDLE_ALL_URLS_RELATION); - - String[] hostList = hosts.split(" "); - if (hostList.length > MAX_HOSTS_PER_REQUEST) { - Log.w(TAG, String.format(TOO_MANY_HOSTS_FORMAT, - hostList.length, MAX_HOSTS_PER_REQUEST)); - sendErrorToPackageManager(context.getPackageManager(), verificationId); - return; - } - - ArrayList finalHosts = new ArrayList(hostList.length); - try { - ArrayList sourceAssets = new ArrayList(); - for (String host : hostList) { - // "*.example.tld" is validated via https://example.tld - if (host.startsWith("*.")) { - host = host.substring(2); - } - sourceAssets.add(createWebAssetString(scheme, host)); - finalHosts.add(host); - } - extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS, - sourceAssets); - } catch (MalformedURLException e) { - Log.w(TAG, "Error when processing input host: " + e.getMessage()); - sendErrorToPackageManager(context.getPackageManager(), verificationId); - return; - } - try { - extras.putString(DirectStatementService.EXTRA_TARGET_ASSET_DESCRIPTOR, - createAndroidAssetString(context, packageName)); - } catch (NameNotFoundException e) { - Log.w(TAG, "Error when processing input Android package: " + e.getMessage()); - sendErrorToPackageManager(context.getPackageManager(), verificationId); - return; - } - extras.putParcelable(DirectStatementService.EXTRA_RESULT_RECEIVER, - new IsAssociatedResultReceiver( - new Handler(), context.getPackageManager(), verificationId)); - - // Required for CTS: log a few details of the validcation operation to be performed - logValidationParametersForCTS(verificationId, scheme, finalHosts, packageName); - - serviceIntent.putExtras(extras); - context.startService(serviceIntent); - } - } else { - Log.w(TAG, "Intent action not supported: " + action); - } - } - - // CTS requirement: logging of the validation parameters in a specific format - private static final String CTS_LOG_FORMAT = - "Verifying IntentFilter. verificationId:%d scheme:\"%s\" hosts:\"%s\" package:\"%s\"."; - private void logValidationParametersForCTS(int verificationId, String scheme, - ArrayList finalHosts, String packageName) { - String hostString = TextUtils.join(" ", finalHosts.toArray()); - Log.i(TAG, String.format(CTS_LOG_FORMAT, verificationId, scheme, hostString, packageName)); - } - - private String createAndroidAssetString(Context context, String packageName) - throws NameNotFoundException { - if (!ANDROID_PACKAGE_NAME_PATTERN.matcher(packageName).matches()) { - throw new NameNotFoundException("Input package name is not valid."); - } - - List certFingerprints = - Utils.getCertFingerprintsFromPackageManager(packageName, context); - - return String.format(ANDROID_ASSET_FORMAT, packageName, - Utils.joinStrings("\", \"", certFingerprints)); - } - - private String createWebAssetString(String scheme, String host) throws MalformedURLException { - if (!Patterns.DOMAIN_NAME.matcher(host).matches()) { - throw new MalformedURLException("Input host is not valid."); - } - if (!scheme.equals("http") && !scheme.equals("https")) { - throw new MalformedURLException("Input scheme is not valid."); - } - - return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString()); - } - - /** - * Receives the result of {@code StatementService.CHECK_ACTION} from - * {@link DirectStatementService} and passes it back to {@link PackageManager}. - */ - private static class IsAssociatedResultReceiver extends ResultReceiver { - - private final int mVerificationId; - private final PackageManager mPackageManager; - - public IsAssociatedResultReceiver(Handler handler, PackageManager packageManager, - int verificationId) { - super(handler); - mVerificationId = verificationId; - mPackageManager = packageManager; - } - - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode == DirectStatementService.RESULT_SUCCESS) { - if (resultData.getBoolean(DirectStatementService.IS_ASSOCIATED)) { - mPackageManager.verifyIntentFilter(mVerificationId, - PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS, - Collections.emptyList()); - } else { - mPackageManager.verifyIntentFilter(mVerificationId, - PackageManager.INTENT_FILTER_VERIFICATION_FAILURE, - resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES)); - } - } else { - sendErrorToPackageManager(mPackageManager, mVerificationId); - } - } - } -} diff --git a/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt new file mode 100644 index 000000000000..021a5143c09a --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice + +import android.app.Application +import android.os.UserManager +import androidx.work.WorkManager +import com.android.statementservice.domain.DomainVerificationUtils + +class StatementServiceApplication : Application() { + + override fun onCreate() { + super.onCreate() + val userManager = getSystemService(UserManager::class.java) ?: return + if (userManager.isUserUnlocked) { + // WorkManager can only schedule when the user data directories are unencrypted (after + // the user has entered their lock password. + DomainVerificationUtils.schedulePeriodicCheckUnlocked(WorkManager.getInstance(this)) + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt b/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt new file mode 100644 index 000000000000..de41486d1996 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.BroadcastReceiver +import android.util.Log +import androidx.work.Constraints +import androidx.work.NetworkType + +abstract class BaseDomainVerificationReceiver : BroadcastReceiver() { + + companion object { + const val DEBUG = false + } + + protected abstract val tag: String + + protected val networkConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + protected fun debugLog(block: () -> String) { + if (DEBUG) { + Log.d(tag, block()) + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt b/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt new file mode 100644 index 000000000000..7b5da832b9d7 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.android.statementservice.domain.worker.RetryRequestWorker + +/** + * Handles [Intent.ACTION_BOOT_COMPLETED] to schedule recurring maintenance [WorkManager] tasks and + * run a one-time retry request to attempt to verify domains that may have failed or been added + * since last device reboot. + * + * Note that this requires the user to have unlocked the device, since [WorkManager] cannot handle + * the encrypted user data directories. + */ +class BootCompletedReceiver : BroadcastReceiver() { + + companion object { + private const val PACKAGE_BOOT_REQUEST_KEY = "package_boot_request" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Intent.ACTION_BOOT_COMPLETED) return + val workManager = WorkManager.getInstance(context) + DomainVerificationUtils.schedulePeriodicCheckUnlocked(workManager) + workManager.beginUniqueWork( + PACKAGE_BOOT_REQUEST_KEY, + ExistingWorkPolicy.REPLACE, + OneTimeWorkRequestBuilder() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + ).enqueue() + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt new file mode 100644 index 000000000000..0ec8ed3416b8 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.work.ExistingWorkPolicy +import androidx.work.WorkManager +import com.android.statementservice.domain.worker.CollectV1Worker +import com.android.statementservice.domain.worker.SingleV1RequestWorker + +/** + * Receiver for V1 API. Separated so that the receiver permission can be declared for only the + * v1 and v2 permissions individually, exactly matching the intended usage. + */ +class DomainVerificationReceiverV1 : BaseDomainVerificationReceiver() { + + companion object { + private const val ENABLE_V1 = true + private const val PACKAGE_WORK_PREFIX_V1 = "package_request_v1-" + } + + override val tag = DomainVerificationReceiverV1::class.java.simpleName + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION -> + scheduleUnlockedV1(context, intent) + else -> debugLog { "Received invalid broadcast: $intent" } + } + } + + private fun scheduleUnlockedV1(context: Context, intent: Intent) { + if (!ENABLE_V1) { + return + } + + val verificationId = + intent.getIntExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID, -1) + val hosts = + (intent.getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS) ?: return) + .split(" ") + val packageName = + intent.getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME) + ?: return + + debugLog { "Attempting v1 verification for $packageName" } + + val workRequests = hosts.map { + SingleV1RequestWorker.buildRequest(packageName, it) { + setConstraints(networkConstraints) + } + } + + WorkManager.getInstance(context) + .beginUniqueWork( + "$PACKAGE_WORK_PREFIX_V1$packageName", + ExistingWorkPolicy.REPLACE, + workRequests + ) + .then(CollectV1Worker.buildRequest(verificationId, packageName)) + .enqueue() + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt new file mode 100644 index 000000000000..24e0f50873c6 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.Context +import android.content.Intent +import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationRequest +import android.os.UserManager +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.WorkManager +import com.android.statementservice.domain.worker.SingleV2RequestWorker +import com.android.statementservice.utils.component1 +import com.android.statementservice.utils.component2 +import com.android.statementservice.utils.component3 + +import java.time.Duration + +/** + * Handles [DomainVerificationRequest]s from the system, which indicates a package on the device + * has domains which require verification against a server side assetlinks.json file, allowing the + * app to resolve web [Intent]s. + * + * This will delegate to v1 or v2 depending on the received broadcast and which components are + * enabled. See [DomainVerificationManager] for the full API. + */ +open class DomainVerificationReceiverV2 : BaseDomainVerificationReceiver() { + + companion object { + + private const val ENABLE_V2 = true + + /** + * Toggle to always re-verify packages that this receiver is notified of. This means on + * every package change, even previously successful requests are re-sent. Generally only + * for debugging. + */ + @Suppress("SimplifyBooleanWithConstants") + private const val ALWAYS_VERIFY = false || DEBUG + + private const val PACKAGE_WORK_PREFIX_V2 = "package_request_v2-" + } + + override val tag = DomainVerificationReceiverV2::class.java.simpleName + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_DOMAINS_NEED_VERIFICATION -> { + // If the user isn't unlocked yet, the request will be ignored, as WorkManager + // cannot schedule workers when the user data directories are encrypted. + if (context.getSystemService(UserManager::class.java)?.isUserUnlocked == true) { + scheduleUnlockedV2(context, intent) + } + } + else -> debugLog { "Received invalid broadcast: $intent" } + } + } + + private fun scheduleUnlockedV2(context: Context, intent: Intent) { + if (!ENABLE_V2) { + return + } + + val manager = context.getSystemService(DomainVerificationManager::class.java) ?: return + val workManager = WorkManager.getInstance(context) + + val request = intent.getParcelableExtra( + DomainVerificationManager.EXTRA_VERIFICATION_REQUEST + ) ?: return + + debugLog { "Attempting v2 verification for ${request.packageNames}" } + + request.packageNames.forEach { packageName -> + val (domainSetId, _, hostToStateMap) = manager.getDomainVerificationInfo(packageName) + ?: return@forEach + + val workRequests = hostToStateMap + .filterValues { + // TODO(b/159952358): Should we support re-query? There's no good way to + // signal to an AOSP implementation from an entity's website about when + // to re-query, unless it's just done on each update. + // AOSP implementation does not support re-query + ALWAYS_VERIFY || VerifyStatus.shouldRetry(it) + } + .map { (host, _) -> + SingleV2RequestWorker.buildRequest(domainSetId, packageName, host) { + setConstraints(networkConstraints) + setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofHours(1)) + } + } + + if (workRequests.isNotEmpty()) { + workManager.beginUniqueWork( + "$PACKAGE_WORK_PREFIX_V2$packageName", + ExistingWorkPolicy.REPLACE, workRequests + ) + .enqueue() + } + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt new file mode 100644 index 000000000000..694424822b30 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.android.statementservice.domain.worker.RetryRequestWorker +import java.time.Duration + +object DomainVerificationUtils { + + private const val PERIODIC_SHORT_ID = "retry_short" + private const val PERIODIC_SHORT_HOURS = 24L + private const val PERIODIC_LONG_ID = "retry_long" + private const val PERIODIC_LONG_HOURS = 72L + + /** + * In a majority of cases, the initial requests will be enough to verify domains, since they + * are also restricted to [NetworkType.CONNECTED], but for cases where they aren't sufficient, + * attempts are also made on a periodic basis. + * + * Once per 24 hours, a check of all packages is done with [NetworkType.CONNECTED]. To avoid + * cases where a proxy or other unusual device configuration prevents [WorkManager] from + * running, also schedule a 3 day task without constraints which will force the check to run. + * + * The actual logic may be skipped if a request was previously run successfully or there are no + * more domains that need verifying. + */ + fun schedulePeriodicCheckUnlocked(workManager: WorkManager) { + workManager.apply { + PeriodicWorkRequestBuilder(Duration.ofHours(PERIODIC_SHORT_HOURS)) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresDeviceIdle(true) + .build() + ) + .build() + .let { + enqueueUniquePeriodicWork( + PERIODIC_SHORT_ID, + ExistingPeriodicWorkPolicy.KEEP, it + ) + } + PeriodicWorkRequestBuilder(Duration.ofDays(PERIODIC_LONG_HOURS)) + .setConstraints( + Constraints.Builder() + .setRequiresDeviceIdle(true) + .build() + ) + .build() + .let { + enqueueUniquePeriodicWork( + PERIODIC_LONG_ID, + ExistingPeriodicWorkPolicy.KEEP, it + ) + } + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt new file mode 100644 index 000000000000..29f844fb1a5d --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.Context +import android.content.pm.verify.domain.DomainVerificationManager +import android.net.Network +import android.util.Log +import androidx.collection.LruCache +import com.android.statementservice.network.retriever.StatementRetriever +import com.android.statementservice.retriever.AbstractAsset +import com.android.statementservice.retriever.AbstractAssetMatcher +import com.android.statementservice.utils.Result +import com.android.statementservice.utils.StatementUtils +import com.android.statementservice.utils.component1 +import com.android.statementservice.utils.component2 +import com.android.statementservice.utils.component3 +import java.net.HttpURLConnection +import java.util.Optional +import java.util.UUID + +private typealias WorkResult = androidx.work.ListenableWorker.Result + +class DomainVerifier private constructor( + private val appContext: Context, + private val manager: DomainVerificationManager +) { + companion object { + private val TAG = DomainVerifier::class.java.simpleName + private const val DEBUG = false + + private var singleton: DomainVerifier? = null + + fun getInstance(context: Context) = when { + singleton != null -> singleton!! + else -> synchronized(this) { + if (singleton == null) { + val appContext = context.applicationContext + val manager = + appContext.getSystemService(DomainVerificationManager::class.java)!! + singleton = DomainVerifier(appContext, manager) + } + singleton!! + } + } + } + + private val retriever = StatementRetriever() + + private val targetAssetCache = AssetLruCache() + + fun collectHosts(packageNames: Iterable): Iterable> { + return packageNames.mapNotNull { packageName -> + val (domainSetId, _, hostToStateMap) = try { + manager.getDomainVerificationInfo(packageName) + } catch (ignored: Exception) { + // Package disappeared, assume it will be rescheduled if the package reappears + null + } ?: return@mapNotNull null + + val hostsToRetry = hostToStateMap + .filterValues(VerifyStatus::shouldRetry) + .takeIf { it.isNotEmpty() } + ?.map { it.key } + ?: return@mapNotNull null + + hostsToRetry.map { Triple(domainSetId, packageName, it) } + } + .flatten() + } + + suspend fun verifyHost( + host: String, + packageName: String, + network: Network? = null + ): Pair { + val assetMatcher = synchronized(targetAssetCache) { targetAssetCache[packageName] } + .takeIf { it!!.isPresent } + ?: return WorkResult.failure() to VerifyStatus.FAILURE_PACKAGE_MANAGER + return verifyHost(host, assetMatcher.get(), network) + } + + private suspend fun verifyHost( + host: String, + assetMatcher: AbstractAssetMatcher, + network: Network? = null + ): Pair { + var exception: Exception? = null + val resultAndStatus = try { + val sourceAsset = StatementUtils.createWebAssetString(host) + .let(AbstractAsset::create) + val result = retriever.retrieve(sourceAsset, network) + ?: return WorkResult.success() to VerifyStatus.FAILURE_UNKNOWN + when (result.responseCode) { + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP -> { + WorkResult.failure() to VerifyStatus.FAILURE_REDIRECT + } + else -> { + val isVerified = result.statements.any { statement -> + (StatementUtils.RELATION.matches(statement.relation) && + assetMatcher.matches(statement.target)) + } + + if (isVerified) { + WorkResult.success() to VerifyStatus.SUCCESS + } else { + WorkResult.failure() to VerifyStatus.FAILURE_REJECTED_BY_SERVER + } + } + } + } catch (e: Exception) { + exception = e + WorkResult.retry() to VerifyStatus.FAILURE_UNKNOWN + } + + if (DEBUG) { + Log.d(TAG, "Verifying $host: ${resultAndStatus.second}", exception) + } + + return resultAndStatus + } + + private inner class AssetLruCache : LruCache>(50) { + override fun create(packageName: String) = + StatementUtils.getCertFingerprintsFromPackageManager(appContext, packageName) + .let { (it as? Result.Success)?.value } + ?.let { StatementUtils.createAndroidAsset(packageName, it) } + ?.let(AbstractAssetMatcher::createMatcher) + .let { Optional.ofNullable(it) } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt new file mode 100644 index 000000000000..2193ec542238 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain + +import android.content.pm.verify.domain.DomainVerificationInfo +import android.content.pm.verify.domain.DomainVerificationManager + +/** + * Wraps known [DomainVerificationManager] status codes so that they can be used in a when + * statement. Unknown codes are coerced to [VerifyStatus.UNKNOWN] and should be treated as + * unverified. + * + * Also includes error codes specific to this implementation of the domain verification agent. + * These must be stable across all versions, as codes are persisted to disk. They do not + * technically have to be stable across different device factory resets, since they will be reset + * once the apps are re-initialized, but easier to keep them unique forever. + */ +enum class VerifyStatus(val value: Int) { + NO_RESPONSE(DomainVerificationInfo.STATE_NO_RESPONSE), + SUCCESS(DomainVerificationInfo.STATE_SUCCESS), + + UNKNOWN(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED), + FAILURE_LEGACY_UNSUPPORTED_WILDCARD(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 1), + FAILURE_REJECTED_BY_SERVER(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 2), + FAILURE_TIMEOUT(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 3), + FAILURE_UNKNOWN(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 4), + FAILURE_REDIRECT(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 5), + + // Failed to retrieve signature information from PackageManager + FAILURE_PACKAGE_MANAGER(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 6); + + companion object { + fun shouldRetry(state: Int): Boolean { + if (state == DomainVerificationInfo.STATE_UNMODIFIABLE) { + return false + } + + val status = values().find { it.value == state } ?: return true + return when (status) { + SUCCESS, + FAILURE_LEGACY_UNSUPPORTED_WILDCARD, + FAILURE_REJECTED_BY_SERVER, + FAILURE_PACKAGE_MANAGER, + UNKNOWN -> false + NO_RESPONSE, + FAILURE_TIMEOUT, + FAILURE_UNKNOWN, + FAILURE_REDIRECT -> true + } + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt new file mode 100644 index 000000000000..a17f9c9186ff --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import android.content.pm.verify.domain.DomainVerificationManager +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.android.statementservice.domain.DomainVerifier + +abstract class BaseRequestWorker( + protected val appContext: Context, + protected val params: WorkerParameters +) : CoroutineWorker(appContext, params) { + + protected val verificationManager = + appContext.getSystemService(DomainVerificationManager::class.java)!! + + protected val verifier = DomainVerifier.getInstance(appContext) +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt new file mode 100644 index 000000000000..3a3aea9288cd --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkerParameters +import com.android.statementservice.utils.AndroidUtils +import kotlinx.coroutines.coroutineScope + +class CollectV1Worker(appContext: Context, params: WorkerParameters) : + BaseRequestWorker(appContext, params) { + + companion object { + private val TAG = CollectV1Worker::class.java.simpleName + private const val DEBUG = false + + private const val VERIFICATION_ID_KEY = "verificationId" + private const val PACKAGE_NAME_KEY = "packageName" + + fun buildRequest(verificationId: Int, packageName: String) = + OneTimeWorkRequestBuilder() + .setInputData( + Data.Builder() + .putInt(VERIFICATION_ID_KEY, verificationId) + .apply { + if (DEBUG) { + putString(PACKAGE_NAME_KEY, packageName) + } + } + .build() + ) + .build() + } + + override suspend fun doWork() = coroutineScope { + if (!AndroidUtils.isReceiverV1Enabled(appContext)) { + return@coroutineScope Result.success() + } + + val inputData = params.inputData + val verificationId = inputData.getInt(VERIFICATION_ID_KEY, -1) + val successfulHosts = mutableListOf() + val failedHosts = mutableListOf() + inputData.keyValueMap.entries.forEach { (key, _) -> + when { + key.startsWith(SingleV1RequestWorker.HOST_SUCCESS_PREFIX) -> + successfulHosts += key.removePrefix(SingleV1RequestWorker.HOST_SUCCESS_PREFIX) + key.startsWith(SingleV1RequestWorker.HOST_FAILURE_PREFIX) -> + failedHosts += key.removePrefix(SingleV1RequestWorker.HOST_FAILURE_PREFIX) + } + } + + if (DEBUG) { + val packageName = inputData.getString(PACKAGE_NAME_KEY) + Log.d( + TAG, "Domain verification v1 request for $packageName: " + + "success = $successfulHosts, failed = $failedHosts" + ) + } + + val resultCode = if (failedHosts.isEmpty()) { + PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS + } else { + PackageManager.INTENT_FILTER_VERIFICATION_FAILURE + } + + appContext.packageManager.verifyIntentFilter(verificationId, resultCode, failedHosts) + + Result.success() + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt new file mode 100644 index 000000000000..61ab2c264e6a --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import androidx.work.NetworkType +import androidx.work.WorkerParameters +import com.android.statementservice.domain.VerifyStatus +import com.android.statementservice.utils.AndroidUtils +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import java.util.UUID + +/** + * Scheduled every 24 hours with [NetworkType.CONNECTED] and every 72 hours without any constraints + * to retry all domains for all packages with a failing error code. + */ +class RetryRequestWorker( + appContext: Context, + params: WorkerParameters +) : BaseRequestWorker(appContext, params) { + + data class VerifyResult(val domainSetId: UUID, val host: String, val status: VerifyStatus) + + override suspend fun doWork() = coroutineScope { + if (!AndroidUtils.isReceiverV2Enabled(appContext)) { + return@coroutineScope Result.success() + } + + val packageNames = verificationManager.queryValidVerificationPackageNames() + + verifier.collectHosts(packageNames) + .map { (domainSetId, packageName, host) -> + async { + if (isActive && !isStopped) { + val (_, status) = verifier.verifyHost(host, packageName, params.network) + VerifyResult(domainSetId, host, status) + } else { + // If the job gets cancelled, stop the remaining hosts, but continue the + // job to commit the results for hosts that were already requested. + null + } + } + } + .awaitAll() + .filterNotNull() // TODO(b/159952358): Fast fail packages which can't be retrieved. + .groupBy { it.domainSetId } + .forEach { (domainSetId, resultsById) -> + resultsById.groupBy { it.status } + .mapValues { it.value.map(VerifyResult::host).toSet() } + .forEach { (status, hosts) -> + verificationManager.setDomainVerificationStatus( + domainSetId, + hosts, + status.value + ) + } + } + + // Succeed regardless of results since this retry is best effort and not required + Result.success() + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt new file mode 100644 index 000000000000..cd8a18218004 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkerParameters +import com.android.statementservice.utils.AndroidUtils +import kotlinx.coroutines.coroutineScope + +class SingleV1RequestWorker(appContext: Context, params: WorkerParameters) : + BaseRequestWorker(appContext, params) { + + companion object { + private val TAG = SingleV1RequestWorker::class.java.simpleName + private const val DEBUG = false + + private const val PACKAGE_NAME_KEY = "packageName" + private const val HOST_KEY = "host" + const val HOST_SUCCESS_PREFIX = "hostSuccess:" + const val HOST_FAILURE_PREFIX = "hostFailure:" + + fun buildRequest( + packageName: String, + host: String, + block: OneTimeWorkRequest.Builder.() -> Unit = {} + ) = OneTimeWorkRequestBuilder() + .setInputData( + Data.Builder() + .putString(PACKAGE_NAME_KEY, packageName) + .putString(HOST_KEY, host) + .build() + ) + .apply(block) + .build() + } + + override suspend fun doWork() = coroutineScope { + if (!AndroidUtils.isReceiverV1Enabled(appContext)) { + return@coroutineScope Result.success() + } + + val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!! + val host = params.inputData.getString(HOST_KEY)!! + + val (result, status) = verifier.verifyHost(host, packageName, params.network) + + if (DEBUG) { + Log.d( + TAG, "Domain verification v1 request for $packageName: " + + "host = $host, status = $status" + ) + } + + // Coerce failure results into success so that final collection task gets a chance to run + when (result) { + is Result.Success -> Result.success( + Data.Builder() + .putInt("$HOST_SUCCESS_PREFIX$host", status.value) + .build() + ) + is Result.Failure -> Result.success( + Data.Builder() + .putInt("$HOST_FAILURE_PREFIX$host", status.value) + .build() + ) + else -> result + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt new file mode 100644 index 000000000000..562b132d36d6 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkerParameters +import com.android.statementservice.utils.AndroidUtils +import kotlinx.coroutines.coroutineScope +import java.util.UUID + +class SingleV2RequestWorker(appContext: Context, params: WorkerParameters) : + BaseRequestWorker(appContext, params) { + + companion object { + private const val DOMAIN_SET_ID_KEY = "domainSetId" + private const val PACKAGE_NAME_KEY = "packageName" + private const val HOST_KEY = "host" + + fun buildRequest( + domainSetId: UUID, + packageName: String, + host: String, + block: OneTimeWorkRequest.Builder.() -> Unit = {} + ) = OneTimeWorkRequestBuilder() + .setInputData( + Data.Builder() + .putString(DOMAIN_SET_ID_KEY, domainSetId.toString()) + .putString(PACKAGE_NAME_KEY, packageName) + .putString(HOST_KEY, host) + .build() + ) + .apply(block) + .build() + } + + override suspend fun doWork() = coroutineScope { + if (!AndroidUtils.isReceiverV2Enabled(appContext)) { + return@coroutineScope Result.success() + } + + val domainSetId = params.inputData.getString(DOMAIN_SET_ID_KEY)!!.let(UUID::fromString) + val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!! + val host = params.inputData.getString(HOST_KEY)!! + + val (result, status) = verifier.verifyHost(host, packageName, params.network) + + verificationManager.setDomainVerificationStatus(domainSetId, setOf(host), status.value) + + result + } +} diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt new file mode 100644 index 000000000000..455e8085af50 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.network.retriever + +import android.util.JsonReader +import com.android.statementservice.retriever.AbstractAsset +import com.android.statementservice.retriever.AssetFactory +import com.android.statementservice.retriever.JsonParser +import com.android.statementservice.retriever.Relation +import com.android.statementservice.retriever.Statement +import com.android.statementservice.utils.Result +import com.android.statementservice.utils.StatementUtils +import java.io.StringReader +import java.util.ArrayList +import com.android.statementservice.retriever.WebAsset +import com.android.statementservice.retriever.AndroidAppAsset + +/** + * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset], + * [AndroidAppAsset], and [Statement]. + */ +object StatementParser { + + private const val FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string." + private const val FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array." + + /** + * Parses a JSON array of statements. + */ + fun parseStatementList(statementList: String, source: AbstractAsset): Result { + val statements: MutableList = ArrayList() + val delegates: MutableList = ArrayList() + StringReader(statementList).use { stringReader -> + JsonReader(stringReader).use { reader -> + reader.isLenient = false + reader.beginArray() + while (reader.hasNext()) { + val result = parseOneStatement(reader, source) + if (result is Result.Failure) { + continue + } + result as Result.Success + statements.addAll(result.value.statements) + delegates.addAll(result.value.delegates) + } + reader.endArray() + } + } + return Result.Success(ParsedStatement(statements, delegates)) + } + + /** + * Parses a single JSON statement. + */ + fun parseStatement(statementString: String, source: AbstractAsset) = + StringReader(statementString).use { stringReader -> + JsonReader(stringReader).use { reader -> + reader.isLenient = false + parseOneStatement(reader, source) + } + } + + /** + * Parses a single JSON statement. This method guarantees that exactly one JSON object + * will be consumed. + */ + private fun parseOneStatement( + reader: JsonReader, + source: AbstractAsset + ): Result { + val statement = JsonParser.parse(reader) + val delegate = statement.optString(StatementUtils.DELEGATE_FIELD_DELEGATE) + if (!delegate.isNullOrEmpty()) { + return Result.Success(ParsedStatement(emptyList(), listOfNotNull(delegate))) + } + + val targetObject = statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET) + ?: return Result.Failure( + FIELD_NOT_STRING_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET) + ) + val relations = statement.optJSONArray(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION) + ?: return Result.Failure( + FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION) + ) + val target = AssetFactory.create(targetObject) + + val statements = (0 until relations.length()) + .map { relations.getString(it) } + .map(Relation::create) + .map { Statement.create(source, target, it) } + return Result.Success(ParsedStatement(statements, listOfNotNull(delegate))) + } + + data class ParsedStatement(val statements: List, val delegates: List) +} diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt new file mode 100644 index 000000000000..c27a26ad0995 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.network.retriever + +import android.content.Intent +import android.net.Network +import com.android.statementservice.retriever.AbstractAsset +import com.android.statementservice.retriever.AndroidAppAsset +import com.android.statementservice.retriever.Statement +import com.android.statementservice.retriever.WebAsset +import com.android.statementservice.utils.StatementUtils.tryOrNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import java.net.URL + +/** + * Retrieves the JSON configured at a given domain that's compliant with the Digital Asset Links + * specification, returning the list of statements which serve as assertions by the web server as + * to what other assets it can be connected with. + * + * Relevant to this app, it allows the website to report which Android app package and signature + * digest has been approved by the website owner, which considers them as the same author and safe + * to automatically delegate web [Intent]s to. + * + * The relevant data classes are [WebAsset], [AndroidAppAsset], and [Statement]. + */ +class StatementRetriever { + + companion object { + private const val HTTP_CONNECTION_TIMEOUT_MILLIS = 5000 + private const val HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = (1024 * 1024).toLong() + private const val MAX_INCLUDE_LEVEL = 1 + private const val WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json" + } + + private val fetcher = UrlFetcher() + + data class Result( + val statements: List, + val responseCode: Int? + ) { + companion object { + val EMPTY = Result(emptyList(), null) + } + + constructor(statements: List, webResult: UrlFetcher.Response) : this( + statements, + webResult.responseCode + ) + } + + suspend fun retrieve(source: AbstractAsset, network: Network? = null) = when (source) { + // TODO:(b/171219506): Does this have to be implemented? + is AndroidAppAsset -> null + is WebAsset -> retrieveFromWeb(source, network) + else -> null + } + + private suspend fun retrieveFromWeb(asset: WebAsset, network: Network? = null): Result? { + val url = computeAssociationJsonUrl(asset) ?: return null + return retrieve(url, MAX_INCLUDE_LEVEL, asset, network) + } + + private fun computeAssociationJsonUrl(asset: WebAsset) = tryOrNull { + URL(asset.scheme, asset.domain, asset.port, WELL_KNOWN_STATEMENT_PATH).toExternalForm() + } + + private suspend fun retrieve( + urlString: String, + maxIncludeLevel: Int, + source: AbstractAsset, + network: Network? = null + ): Result { + if (maxIncludeLevel < 0) { + return Result.EMPTY + } + + return withContext(Dispatchers.IO) { + val url = try { + @Suppress("BlockingMethodInNonBlockingContext") + URL(urlString) + } catch (ignored: Exception) { + return@withContext Result.EMPTY + } + + val webResponse = fetcher.fetch( + url = url, + connectionTimeoutMillis = HTTP_CONNECTION_TIMEOUT_MILLIS, + fileSizeLimit = HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, + network + ).successValueOrNull() ?: return@withContext Result.EMPTY + + val content = webResponse.content ?: return@withContext Result(emptyList(), webResponse) + val (statements, delegates) = StatementParser.parseStatementList(content, source) + .successValueOrNull() ?: return@withContext Result(emptyList(), webResponse) + + val delegatedStatements = delegates + .map { async { retrieve(it, maxIncludeLevel - 1, source).statements } } + .awaitAll() + .flatten() + + Result(statements + delegatedStatements, webResponse) + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt new file mode 100644 index 000000000000..5c1f5e09b957 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.network.retriever + +import android.net.Network +import android.net.TrafficStats +import android.util.Log +import com.android.statementservice.utils.Result +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import java.net.HttpURLConnection +import java.net.URL +import java.nio.charset.Charset +import javax.net.ssl.HttpsURLConnection + +class UrlFetcher { + + companion object { + private val TAG = UrlFetcher::class.java.simpleName + } + + suspend fun fetch( + url: URL, + connectionTimeoutMillis: Int, + fileSizeLimit: Long, + network: Network? = null + ) = withContext(Dispatchers.IO) { + TrafficStats.setThreadStatsTag(Thread.currentThread().id.toInt()) + @Suppress("BlockingMethodInNonBlockingContext") + val connection = + ((network?.openConnection(url) ?: url.openConnection()) as HttpsURLConnection) + try { + connection.apply { + connectTimeout = connectionTimeoutMillis + readTimeout = connectionTimeoutMillis + useCaches = true + instanceFollowRedirects = false + addRequestProperty("Cache-Control", "max-stale=60") + } + val responseCode = connection.responseCode + when { + responseCode != HttpURLConnection.HTTP_OK -> { + Log.w(TAG, "The responses code is not 200 but $responseCode") + Result.Success(Response(responseCode)) + } + connection.contentLength > fileSizeLimit -> { + Log.w(TAG, "The content size of the url is larger than $fileSizeLimit") + Result.Success(Response(responseCode)) + } + else -> { + val content = async { + connection.inputStream + .bufferedReader(Charset.forName("UTF-8")) + .readText() + } + + Result.Success(Response(responseCode, content.await())) + } + } + } catch (ignored: Throwable) { + Result.Failure(ignored) + } finally { + connection.disconnect() + } + } + + data class Response( + val responseCode: Int, + val content: String? = null + ) +} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java index 8d6fd66db498..4834626edbb6 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java @@ -81,6 +81,9 @@ public abstract class AbstractAsset { /** * If this is the source asset of a statement file, should the retriever follow * any insecure (non-HTTPS) include statements made by the asset. + * + * TODO(b/171219506): Why would this be allowed? Can it be removed, even for web assets? + * Android doesn't even allow non-secure traffic by default. */ public abstract boolean followInsecureInclude(); } diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java deleted file mode 100644 index fe9b99a0a976..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import android.content.Context; -import android.annotation.NonNull; - -import java.util.List; - -/** - * Retrieves the statements made by assets. This class is the entry point of the package. - *

- * An asset is an identifiable and addressable online entity that typically - * provides some service or content. Examples of assets are websites, Android - * apps, Twitter feeds, and Plus Pages. - *

- * Ownership of an asset is defined by being able to control it and speak for it. - * An asset owner may establish a relationship between the asset and another - * asset by making a statement about an intended relationship between the two. - * An example of a relationship is permission delegation. For example, the owner - * of a website (the webmaster) may delegate the ability the handle URLs to a - * particular mobile app. Relationships are considered public information. - *

- * A particular kind of relationship (like permission delegation) defines a binary - * relation on assets. The relation is not symmetric or transitive, nor is it - * antisymmetric or anti-transitive. - *

- * A statement S(r, a, b) is an assertion that the relation r holds for the - * ordered pair of assets (a, b). For example, taking r = "delegates permission - * to view user's location", a = New York Times mobile app, - * b = nytimes.com website, S(r, a, b) would be an assertion that "the New York - * Times mobile app delegates its ability to use the user's location to the - * nytimes.com website". - *

- * A statement S(r, a, b) is considered reliable if we have confidence that - * the statement is true; the exact criterion depends on the kind of statement, - * since some kinds of statements may be true on their face whereas others may - * require multiple parties to agree. - *

- * For example, to get the statements made by www.example.com use: - *

- * result = retrieveStatements(AssetFactory.create(
- *     "{\"namespace\": \"web\", \"site\": \"https://www.google.com\"}"))
- * 
- * {@code result} will contain the statements and the expiration time of this result. The statements - * are considered reliable until the expiration time. - */ -public abstract class AbstractStatementRetriever { - - /** - * Returns the statements made by the {@code source} asset with ttl. - * - * @throws AssociationServiceException if the asset namespace is not supported. - */ - public abstract Result retrieveStatements(AbstractAsset source) - throws AssociationServiceException; - - /** - * The retrieved statements and the expiration date. - */ - public interface Result { - - /** - * @return the retrieved statements. - */ - @NonNull - public List getStatements(); - - /** - * @return the expiration time in millisecond. - */ - public long getExpireMillis(); - } - - /** - * Creates a new StatementRetriever that directly retrieves statements from the asset. - * - *

For web assets, {@link AbstractStatementRetriever} will try to retrieve the statement - * file from URL: {@code [webAsset.site]/.well-known/assetlinks.json"} where {@code - * [webAsset.site]} is in the form {@code http{s}://[hostname]:[optional_port]}. The file - * should contain one JSON array of statements. - * - *

For Android assets, {@link AbstractStatementRetriever} will try to retrieve the statement - * from the AndroidManifest.xml. The developer should add a {@code meta-data} tag under - * {@code application} tag where attribute {@code android:name} equals "associated_assets" - * and {@code android:recourse} points to a string array resource. Each entry in the string - * array should contain exactly one statement in JSON format. Note that this implementation - * can only return statements made by installed apps. - */ - public static AbstractStatementRetriever createDirectRetriever(Context context) { - return new DirectStatementRetriever(new URLFetcher(), - new AndroidPackageInfoFetcher(context)); - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java index 8ead90b88faf..14ca23243b4b 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java @@ -16,6 +16,8 @@ package com.android.statementservice.retriever; +import com.android.statementservice.utils.StatementUtils; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -34,7 +36,8 @@ import java.util.Locale; * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] } * *

For example, { "namespace": "android_app", "package_name": "com.test.mytestapp", - * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"] + * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D + * :7F:D4:A9:16:10:11:AB:92:B9:8F:3F"] * } * *

Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: @@ -43,7 +46,7 @@ import java.util.Locale; *

Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...) * representing the certificate SHA-256 fingerprint. */ -/* package private */ final class AndroidAppAsset extends AbstractAsset { +public final class AndroidAppAsset extends AbstractAsset { private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set."; private static final String MISSING_APPCERTS_FORMAT_STRING = @@ -65,9 +68,10 @@ import java.util.Locale; public String toJson() { AssetJsonWriter writer = new AssetJsonWriter(); - writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP); - writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName); - writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints); + writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD, + StatementUtils.NAMESPACE_ANDROID_APP); + writer.writeFieldLower(StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName); + writer.writeArrayUpper(StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints); return writer.closeAndGetString(); } @@ -114,17 +118,17 @@ import java.util.Locale; */ public static AndroidAppAsset create(JSONObject asset) throws AssociationServiceException { - String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME); + String packageName = asset.optString(StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME); if (packageName.equals("")) { throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING, - Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME)); + StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME)); } - JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS); + JSONArray certArray = asset.optJSONArray(StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS); if (certArray == null || certArray.length() == 0) { throw new AssociationServiceException( String.format(MISSING_APPCERTS_FORMAT_STRING, - Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); + StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); } List certFingerprints = new ArrayList<>(certArray.length()); for (int i = 0; i < certArray.length(); i++) { @@ -133,7 +137,7 @@ import java.util.Locale; } catch (JSONException e) { throw new AssociationServiceException( String.format(APPCERT_NOT_STRING_FORMAT_STRING, - Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); + StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); } } @@ -143,7 +147,7 @@ import java.util.Locale; /** * Creates a new AndroidAppAsset. * - * @param packageName the package name of the Android app. + * @param packageName the package name of the Android app. * @param certFingerprints at least one of the Android app signing certificate sha-256 * fingerprint. */ diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java index 8a9d838c7fed..45798faf91ff 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java @@ -23,7 +23,7 @@ import java.util.Set; * Match assets that have the same 'package_name' field and have at least one common certificate * fingerprint in 'sha256_cert_fingerprints' field. */ -/* package private */ final class AndroidAppAssetMatcher extends AbstractAssetMatcher { +public final class AndroidAppAssetMatcher extends AbstractAssetMatcher { private final AndroidAppAsset mQuery; diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java deleted file mode 100644 index 1000c4c6e5b2..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources.NotFoundException; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * Class that provides information about an android app from {@link PackageManager}. - * - * Visible for testing. - * - * @hide - */ -public class AndroidPackageInfoFetcher { - - /** - * The name of the metadata tag in AndroidManifest.xml that stores the associated asset array - * ID. The metadata tag should use the android:resource attribute to point to an array resource - * that contains the associated assets. - */ - private static final String ASSOCIATED_ASSETS_KEY = "associated_assets"; - - private Context mContext; - - public AndroidPackageInfoFetcher(Context context) { - mContext = context; - } - - /** - * Returns the Sha-256 fingerprints of all certificates from the specified package as a list of - * upper case HEX Strings with bytes separated by colons. Given an app {@link - * android.content.pm.Signature}, the fingerprint can be computed as {@link - * Utils#computeNormalizedSha256Fingerprint} {@code(signature.toByteArray())}. - * - *

Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: {@code - * keytool -list -printcert -jarfile signed_app.apk} - * - *

Example: "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1" - * - * @throws NameNotFoundException if an app with packageName is not installed on the device. - */ - public List getCertFingerprints(String packageName) throws NameNotFoundException { - return Utils.getCertFingerprintsFromPackageManager(packageName, mContext); - } - - /** - * Returns all statements that the specified package makes in its AndroidManifest.xml. - * - * @throws NameNotFoundException if the app is not installed on the device. - */ - public List getStatements(String packageName) throws NameNotFoundException { - PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo( - packageName, PackageManager.GET_META_DATA); - ApplicationInfo appInfo = packageInfo.applicationInfo; - if (appInfo.metaData == null) { - return Collections.emptyList(); - } - int tokenResourceId = appInfo.metaData.getInt(ASSOCIATED_ASSETS_KEY); - if (tokenResourceId == 0) { - return Collections.emptyList(); - } - try { - return Arrays.asList( - mContext.getPackageManager().getResourcesForApplication(packageName) - .getStringArray(tokenResourceId)); - } catch (NotFoundException e) { - return Collections.emptyList(); - } - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java index 519d73a22ca6..ac0bfabfbd44 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java @@ -16,12 +16,14 @@ package com.android.statementservice.retriever; +import com.android.statementservice.utils.StatementUtils; + import org.json.JSONObject; /** * Factory to create asset from JSON string. */ -/* package private */ final class AssetFactory { +public final class AssetFactory { private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."; @@ -34,15 +36,15 @@ import org.json.JSONObject; */ public static AbstractAsset create(JSONObject asset) throws AssociationServiceException { - String namespace = asset.optString(Utils.NAMESPACE_FIELD, null); + String namespace = asset.optString(StatementUtils.NAMESPACE_FIELD, null); if (namespace == null) { throw new AssociationServiceException(String.format( - FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD)); + FIELD_NOT_STRING_FORMAT_STRING, StatementUtils.NAMESPACE_FIELD)); } - if (namespace.equals(Utils.NAMESPACE_WEB)) { + if (namespace.equals(StatementUtils.NAMESPACE_WEB)) { return WebAsset.create(asset); - } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) { + } else if (namespace.equals(StatementUtils.NAMESPACE_ANDROID_APP)) { return AndroidAppAsset.create(asset); } else { throw new AssociationServiceException("Namespace " + namespace + " is not supported."); diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java index 1a50757d932f..7773668551a5 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java @@ -16,6 +16,8 @@ package com.android.statementservice.retriever; +import com.android.statementservice.utils.StatementUtils; + import org.json.JSONException; import org.json.JSONObject; @@ -31,15 +33,15 @@ import org.json.JSONObject; JSONException { JSONObject queryObject = new JSONObject(query); - String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null); + String namespace = queryObject.optString(StatementUtils.NAMESPACE_FIELD, null); if (namespace == null) { throw new AssociationServiceException(String.format( - FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD)); + FIELD_NOT_STRING_FORMAT_STRING, StatementUtils.NAMESPACE_FIELD)); } - if (namespace.equals(Utils.NAMESPACE_WEB)) { + if (namespace.equals(StatementUtils.NAMESPACE_WEB)) { return new WebAssetMatcher(WebAsset.create(queryObject)); - } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) { + } else if (namespace.equals(StatementUtils.NAMESPACE_ANDROID_APP)) { return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject)); } else { throw new AssociationServiceException( diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java deleted file mode 100644 index 9839329bf34c..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import android.content.pm.PackageManager.NameNotFoundException; -import android.util.Log; - -import org.json.JSONException; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * An implementation of {@link AbstractStatementRetriever} that directly retrieves statements from - * the asset. - */ -/* package private */ final class DirectStatementRetriever extends AbstractStatementRetriever { - - private static final long DO_NOT_CACHE_RESULT = 0L; - private static final int HTTP_CONNECTION_TIMEOUT_MILLIS = 5000; - private static final int HTTP_CONNECTION_BACKOFF_MILLIS = 3000; - private static final int HTTP_CONNECTION_RETRY = 3; - private static final long HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024; - private static final int MAX_INCLUDE_LEVEL = 1; - private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json"; - - private final URLFetcher mUrlFetcher; - private final AndroidPackageInfoFetcher mAndroidFetcher; - - /** - * An immutable value type representing the retrieved statements and the expiration date. - */ - public static class Result implements AbstractStatementRetriever.Result { - - private final List mStatements; - private final Long mExpireMillis; - - @Override - public List getStatements() { - return mStatements; - } - - @Override - public long getExpireMillis() { - return mExpireMillis; - } - - private Result(List statements, Long expireMillis) { - mStatements = statements; - mExpireMillis = expireMillis; - } - - public static Result create(List statements, Long expireMillis) { - return new Result(statements, expireMillis); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append("Result: "); - result.append(mStatements.toString()); - result.append(", mExpireMillis="); - result.append(mExpireMillis); - return result.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Result result = (Result) o; - - if (!mExpireMillis.equals(result.mExpireMillis)) { - return false; - } - if (!mStatements.equals(result.mStatements)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = mStatements.hashCode(); - result = 31 * result + mExpireMillis.hashCode(); - return result; - } - } - - public DirectStatementRetriever(URLFetcher urlFetcher, - AndroidPackageInfoFetcher androidFetcher) { - this.mUrlFetcher = urlFetcher; - this.mAndroidFetcher = androidFetcher; - } - - @Override - public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException { - if (source instanceof AndroidAppAsset) { - return retrieveFromAndroid((AndroidAppAsset) source); - } else if (source instanceof WebAsset) { - return retrieveFromWeb((WebAsset) source); - } else { - throw new AssociationServiceException("Namespace is not supported."); - } - } - - private String computeAssociationJsonUrl(WebAsset asset) { - try { - return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(), - WELL_KNOWN_STATEMENT_PATH) - .toExternalForm(); - } catch (MalformedURLException e) { - throw new AssertionError("Invalid domain name in database."); - } - } - - private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel, - AbstractAsset source) - throws AssociationServiceException { - List statements = new ArrayList(); - if (maxIncludeLevel < 0) { - return Result.create(statements, DO_NOT_CACHE_RESULT); - } - - WebContent webContent; - try { - URL url = new URL(urlString); - if (!source.followInsecureInclude() - && !url.getProtocol().toLowerCase().equals("https")) { - return Result.create(statements, DO_NOT_CACHE_RESULT); - } - webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url, - HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS, - HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY); - } catch (IOException | InterruptedException e) { - return Result.create(statements, DO_NOT_CACHE_RESULT); - } - - try { - ParsedStatement result = StatementParser - .parseStatementList(webContent.getContent(), source); - statements.addAll(result.getStatements()); - for (String delegate : result.getDelegates()) { - statements.addAll( - retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source) - .getStatements()); - } - return Result.create(statements, webContent.getExpireTimeMillis()); - } catch (JSONException | IOException e) { - return Result.create(statements, DO_NOT_CACHE_RESULT); - } - } - - private Result retrieveFromWeb(WebAsset asset) - throws AssociationServiceException { - return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset); - } - - private Result retrieveFromAndroid(AndroidAppAsset asset) throws AssociationServiceException { - try { - List delegates = new ArrayList(); - List statements = new ArrayList(); - - List certFps = mAndroidFetcher.getCertFingerprints(asset.getPackageName()); - if (!Utils.hasCommonString(certFps, asset.getCertFingerprints())) { - throw new AssociationServiceException( - "Specified certs don't match the installed app."); - } - - AndroidAppAsset actualSource = AndroidAppAsset.create(asset.getPackageName(), certFps); - for (String statementJson : mAndroidFetcher.getStatements(asset.getPackageName())) { - ParsedStatement result = - StatementParser.parseStatement(statementJson, actualSource); - statements.addAll(result.getStatements()); - delegates.addAll(result.getDelegates()); - } - - for (String delegate : delegates) { - statements.addAll(retrieveStatementFromUrl(delegate, MAX_INCLUDE_LEVEL, - actualSource).getStatements()); - } - - return Result.create(statements, DO_NOT_CACHE_RESULT); - } catch (JSONException | IOException | NameNotFoundException e) { - Log.w(DirectStatementRetriever.class.getSimpleName(), e); - return Result.create(Collections.emptyList(), DO_NOT_CACHE_RESULT); - } - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java b/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java deleted file mode 100644 index 9446e660978a..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import java.util.List; - -/** - * A class that stores a list of statement and/or a list of delegate url. - */ -/* package private */ final class ParsedStatement { - - private final List mStatements; - private final List mDelegates; - - public ParsedStatement(List statements, List delegates) { - this.mStatements = statements; - this.mDelegates = delegates; - } - - public List getStatements() { - return mStatements; - } - - public List getDelegates() { - return mDelegates; - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java index 0f40a6221017..f8bab3ef170f 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java @@ -17,6 +17,11 @@ package com.android.statementservice.retriever; import android.annotation.NonNull; +import android.net.Network; + +import com.android.statementservice.network.retriever.StatementRetriever; + +import kotlin.coroutines.Continuation; /** * An immutable value type representing a statement, consisting of a source, target, and relation. @@ -31,9 +36,9 @@ import android.annotation.NonNull; * } * * - * Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a - * {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation} - * equal to + * Then invoking {@link StatementRetriever#retrieve(AbstractAsset, Network, Continuation)} will + * return a {@link Statement} with {@link #getSource} equal to the input parameter, + * {@link #getRelation} equal to * *

Relation.create("delegate_permission", "common.get_login_creds");
* diff --git a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java deleted file mode 100644 index 0369718ee507..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import android.util.JsonReader; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; - -/** - * Utility class that parses JSON-formatted statements. - */ -/* package private */ final class StatementParser { - - private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."; - private static final String FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array."; - - /** - * Parses a JSON array of statements. - */ - static ParsedStatement parseStatementList(String statementList, AbstractAsset source) - throws JSONException, IOException { - List statements = new ArrayList(); - List delegates = new ArrayList(); - - JsonReader reader = new JsonReader(new StringReader(statementList)); - reader.setLenient(false); - - reader.beginArray(); - while (reader.hasNext()) { - ParsedStatement result; - try { - result = parseStatement(reader, source); - } catch (AssociationServiceException e) { - // The element in the array is well formatted Json but not a well-formed Statement. - continue; - } - statements.addAll(result.getStatements()); - delegates.addAll(result.getDelegates()); - } - reader.endArray(); - - return new ParsedStatement(statements, delegates); - } - - /** - * Parses a single JSON statement. - */ - static ParsedStatement parseStatement(String statementString, AbstractAsset source) - throws AssociationServiceException, IOException, JSONException { - JsonReader reader = new JsonReader(new StringReader(statementString)); - reader.setLenient(false); - return parseStatement(reader, source); - } - - /** - * Parses a single JSON statement. This method guarantees that exactly one JSON object - * will be consumed. - */ - static ParsedStatement parseStatement(JsonReader reader, AbstractAsset source) - throws JSONException, AssociationServiceException, IOException { - List statements = new ArrayList(); - List delegates = new ArrayList(); - - JSONObject statement = JsonParser.parse(reader); - - if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) { - delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE)); - } else { - JSONObject targetObject = statement.optJSONObject(Utils.ASSET_DESCRIPTOR_FIELD_TARGET); - if (targetObject == null) { - throw new AssociationServiceException(String.format( - FIELD_NOT_STRING_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_TARGET)); - } - - JSONArray relations = statement.optJSONArray(Utils.ASSET_DESCRIPTOR_FIELD_RELATION); - if (relations == null) { - throw new AssociationServiceException(String.format( - FIELD_NOT_ARRAY_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_RELATION)); - } - - AbstractAsset target = AssetFactory.create(targetObject); - for (int i = 0; i < relations.length(); i++) { - statements.add(Statement - .create(source, target, Relation.create(relations.getString(i)))); - } - } - - return new ParsedStatement(statements, delegates); - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java deleted file mode 100644 index 23cd83221fdc..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.statementservice.retriever; - -import android.util.Log; - -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.toolbox.HttpHeaderParser; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -/** - * Helper class for fetching HTTP or HTTPS URL. - * - * Visible for testing. - * - * @hide - */ -public class URLFetcher { - private static final String TAG = URLFetcher.class.getSimpleName(); - - private static final long DO_NOT_CACHE_RESULT = 0L; - private static final int INPUT_BUFFER_SIZE_IN_BYTES = 1024; - - /** - * Fetches the specified url and returns the content and ttl. - * - *

- * Retry {@code retry} times if the connection failed or timed out for any reason. - * HTTP error code (e.g. 404/500) won't be retried. - * - * @throws IOException if it can't retrieve the content due to a network problem. - * @throws AssociationServiceException if the URL scheme is not http or https or the content - * length exceeds {code fileSizeLimit}. - */ - public WebContent getWebContentFromUrlWithRetry(URL url, long fileSizeLimit, - int connectionTimeoutMillis, int backoffMillis, int retry) - throws AssociationServiceException, IOException, InterruptedException { - if (retry <= 0) { - throw new IllegalArgumentException("retry should be a postive inetger."); - } - while (retry > 0) { - try { - return getWebContentFromUrl(url, fileSizeLimit, connectionTimeoutMillis); - } catch (IOException e) { - retry--; - if (retry == 0) { - throw e; - } - } - - Thread.sleep(backoffMillis); - } - - // Should never reach here. - return null; - } - - /** - * Fetches the specified url and returns the content and ttl. - * - * @throws IOException if it can't retrieve the content due to a network problem. - * @throws AssociationServiceException if the URL scheme is not http or https or the content - * length exceeds {code fileSizeLimit}. - */ - public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis) - throws AssociationServiceException, IOException { - final String scheme = url.getProtocol().toLowerCase(Locale.US); - if (!scheme.equals("http") && !scheme.equals("https")) { - throw new IllegalArgumentException("The url protocol should be on http or https."); - } - - HttpURLConnection connection = null; - try { - connection = (HttpURLConnection) url.openConnection(); - connection.setInstanceFollowRedirects(true); - connection.setConnectTimeout(connectionTimeoutMillis); - connection.setReadTimeout(connectionTimeoutMillis); - connection.setUseCaches(true); - connection.setInstanceFollowRedirects(false); - connection.addRequestProperty("Cache-Control", "max-stale=60"); - - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.e(TAG, "The responses code is not 200 but " + connection.getResponseCode()); - return new WebContent("", DO_NOT_CACHE_RESULT); - } - - if (connection.getContentLength() > fileSizeLimit) { - Log.e(TAG, "The content size of the url is larger than " + fileSizeLimit); - return new WebContent("", DO_NOT_CACHE_RESULT); - } - - Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader( - connection.getHeaderFields()); - - return new WebContent(inputStreamToString( - connection.getInputStream(), connection.getContentLength(), fileSizeLimit), - expireTimeMillis); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - /** - * Visible for testing. - * @hide - */ - public static String inputStreamToString(InputStream inputStream, int length, long sizeLimit) - throws IOException, AssociationServiceException { - if (length < 0) { - length = 0; - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(length); - BufferedInputStream bis = new BufferedInputStream(inputStream); - byte[] buffer = new byte[INPUT_BUFFER_SIZE_IN_BYTES]; - int len = 0; - while ((len = bis.read(buffer)) != -1) { - baos.write(buffer, 0, len); - if (baos.size() > sizeLimit) { - throw new AssociationServiceException("The content size of the url is larger than " - + sizeLimit); - } - } - return baos.toString("UTF-8"); - } - - /** - * Parses the HTTP headers to compute the ttl. - * - * @param headers a map that map the header key to the header values. Can be null. - * @return the ttl in millisecond or null if the ttl is not specified in the header. - */ - private Long getExpirationTimeMillisFromHTTPHeader(Map> headers) { - if (headers == null) { - return null; - } - Map joinedHeaders = joinHttpHeaders(headers); - - NetworkResponse response = new NetworkResponse(null, joinedHeaders); - Cache.Entry cachePolicy = HttpHeaderParser.parseCacheHeaders(response); - - if (cachePolicy == null) { - // Cache is disabled, set the expire time to 0. - return DO_NOT_CACHE_RESULT; - } else if (cachePolicy.ttl == 0) { - // Cache policy is not specified, set the expire time to 0. - return DO_NOT_CACHE_RESULT; - } else { - // cachePolicy.ttl is actually the expire timestamp in millisecond. - return cachePolicy.ttl; - } - } - - /** - * Converts an HTTP header map of the format provided by {@linkHttpUrlConnection} to a map of - * the format accepted by {@link HttpHeaderParser}. It does this by joining all the entries for - * a given header key with ", ". - */ - private Map joinHttpHeaders(Map> headers) { - Map joinedHeaders = new HashMap(); - for (Map.Entry> entry : headers.entrySet()) { - List values = entry.getValue(); - if (values.size() == 1) { - joinedHeaders.put(entry.getKey(), values.get(0)); - } else { - joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values)); - } - } - return joinedHeaders; - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Utils.java b/packages/StatementService/src/com/android/statementservice/retriever/Utils.java deleted file mode 100644 index afb4c7507401..000000000000 --- a/packages/StatementService/src/com/android/statementservice/retriever/Utils.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.statementservice.retriever; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -/** - * Utility library for computing certificate fingerprints. Also includes fields name used by - * Statement JSON string. - */ -public final class Utils { - - private Utils() {} - - /** - * Field name for namespace. - */ - public static final String NAMESPACE_FIELD = "namespace"; - - /** - * Supported asset namespaces. - */ - public static final String NAMESPACE_WEB = "web"; - public static final String NAMESPACE_ANDROID_APP = "android_app"; - - /** - * Field names in a web asset descriptor. - */ - public static final String WEB_ASSET_FIELD_SITE = "site"; - - /** - * Field names in a Android app asset descriptor. - */ - public static final String ANDROID_APP_ASSET_FIELD_PACKAGE_NAME = "package_name"; - public static final String ANDROID_APP_ASSET_FIELD_CERT_FPS = "sha256_cert_fingerprints"; - - /** - * Field names in a statement. - */ - public static final String ASSET_DESCRIPTOR_FIELD_RELATION = "relation"; - public static final String ASSET_DESCRIPTOR_FIELD_TARGET = "target"; - public static final String DELEGATE_FIELD_DELEGATE = "include"; - - private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F' }; - - /** - * Joins a list of strings, by placing separator between each string. For example, - * {@code joinStrings("; ", Arrays.asList(new String[]{"a", "b", "c"}))} returns - * "{@code a; b; c}". - */ - public static String joinStrings(String separator, List strings) { - switch(strings.size()) { - case 0: - return ""; - case 1: - return strings.get(0); - default: - StringBuilder joiner = new StringBuilder(); - boolean first = true; - for (String field : strings) { - if (first) { - first = false; - } else { - joiner.append(separator); - } - joiner.append(field); - } - return joiner.toString(); - } - } - - /** - * Returns the normalized sha-256 fingerprints of a given package according to the Android - * package manager. - */ - public static List getCertFingerprintsFromPackageManager(String packageName, - Context context) throws NameNotFoundException { - Signature[] signatures = context.getPackageManager().getPackageInfo(packageName, - PackageManager.GET_SIGNATURES).signatures; - ArrayList result = new ArrayList(signatures.length); - for (Signature sig : signatures) { - result.add(computeNormalizedSha256Fingerprint(sig.toByteArray())); - } - return result; - } - - /** - * Computes the hash of the byte array using the specified algorithm, returning a hex string - * with a colon between each byte. - */ - public static String computeNormalizedSha256Fingerprint(byte[] signature) { - MessageDigest digester; - try { - digester = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("No SHA-256 implementation found."); - } - digester.update(signature); - return byteArrayToHexString(digester.digest()); - } - - /** - * Returns true if there is at least one common string between the two lists of string. - */ - public static boolean hasCommonString(List list1, List list2) { - HashSet set2 = new HashSet<>(list2); - for (String string : list1) { - if (set2.contains(string)) { - return true; - } - } - return false; - } - - /** - * Converts the byte array to an lowercase hexadecimal digits String with a colon character (:) - * between each byte. - */ - private static String byteArrayToHexString(byte[] array) { - if (array.length == 0) { - return ""; - } - char[] buf = new char[array.length * 3 - 1]; - - int bufIndex = 0; - for (int i = 0; i < array.length; i++) { - byte b = array[i]; - if (i > 0) { - buf[bufIndex++] = ':'; - } - buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F]; - buf[bufIndex++] = HEX_DIGITS[b & 0x0F]; - } - return new String(buf); - } -} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java index 947087a553f5..608ce6926efa 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java @@ -16,6 +16,8 @@ package com.android.statementservice.retriever; +import com.android.statementservice.utils.StatementUtils; + import org.json.JSONObject; import java.net.MalformedURLException; @@ -36,7 +38,7 @@ import java.util.Locale; *

The only protocol supported now are https and http. If the optional port is not specified, * the default for each protocol will be used (i.e. 80 for http and 443 for https). */ -/* package private */ final class WebAsset extends AbstractAsset { +public final class WebAsset extends AbstractAsset { private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set."; private static final String SCHEME_HTTP = "http"; @@ -73,8 +75,8 @@ import java.util.Locale; public String toJson() { AssetJsonWriter writer = new AssetJsonWriter(); - writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB); - writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm()); + writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD, StatementUtils.NAMESPACE_WEB); + writer.writeFieldLower(StatementUtils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm()); return writer.closeAndGetString(); } @@ -119,14 +121,14 @@ import java.util.Locale; */ protected static WebAsset create(JSONObject asset) throws AssociationServiceException { - if (asset.optString(Utils.WEB_ASSET_FIELD_SITE).equals("")) { + if (asset.optString(StatementUtils.WEB_ASSET_FIELD_SITE).equals("")) { throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING, - Utils.WEB_ASSET_FIELD_SITE)); + StatementUtils.WEB_ASSET_FIELD_SITE)); } URL url; try { - url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE)); + url = new URL(asset.optString(StatementUtils.WEB_ASSET_FIELD_SITE)); } catch (MalformedURLException e) { throw new AssociationServiceException("Url is not well formatted.", e); } diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java index 86a635c16436..23b1f9b894f4 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java @@ -27,10 +27,12 @@ public final class WebContent { private final String mContent; private final Long mExpireTimeMillis; + private final int mResponseCode; - public WebContent(String content, Long expireTimeMillis) { + public WebContent(String content, Long expireTimeMillis, int responseCode) { mContent = content; mExpireTimeMillis = expireTimeMillis; + mResponseCode = responseCode; } /** @@ -46,4 +48,8 @@ public final class WebContent { public String getContent() { return mContent; } + + public int getResponseCode() { + return mResponseCode; + } } diff --git a/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt new file mode 100644 index 000000000000..7fe0a029088b --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.utils + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.verify.domain.DomainVerificationInfo +import android.content.pm.verify.domain.DomainVerificationRequest +import android.content.pm.verify.domain.DomainVerificationUserState +import com.android.statementservice.domain.DomainVerificationReceiverV1 +import com.android.statementservice.domain.DomainVerificationReceiverV2 + +// Top level extensions for models to allow Kotlin deconstructing declarations + +operator fun DomainVerificationRequest.component1() = packageNames + +operator fun DomainVerificationInfo.component1() = identifier +operator fun DomainVerificationInfo.component2() = packageName +operator fun DomainVerificationInfo.component3() = hostToStateMap + +operator fun DomainVerificationUserState.component1() = identifier +operator fun DomainVerificationUserState.component2() = packageName +operator fun DomainVerificationUserState.component3() = user +operator fun DomainVerificationUserState.component4() = isLinkHandlingAllowed +operator fun DomainVerificationUserState.component5() = hostToStateMap + +object AndroidUtils { + + fun isReceiverV1Enabled(context: Context): Boolean { + val receiver = ComponentName(context, DomainVerificationReceiverV1::class.java) + return when (context.packageManager.getComponentEnabledSetting(receiver)) { + // Must change this if the manifest ever changes + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> true + PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true + else -> false + } + } + + fun isReceiverV2Enabled(context: Context): Boolean { + val receiver = ComponentName(context, DomainVerificationReceiverV2::class.java) + return when (context.packageManager.getComponentEnabledSetting(receiver)) { + // Must change this if the manifest ever changes + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> false + PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true + else -> false + } + } +} diff --git a/packages/StatementService/src/com/android/statementservice/utils/Result.kt b/packages/StatementService/src/com/android/statementservice/utils/Result.kt new file mode 100644 index 000000000000..f23a010744e9 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/utils/Result.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.utils + +sealed class Result { + + fun successValueOrNull() = (this as? Success)?.value + + data class Success(val value: T) : Result() + data class Failure(val message: String? = null, val throwable: Throwable? = null) : + Result() { + + constructor(message: String) : this(message = message, throwable = null) + constructor(throwable: Throwable) : this(message = null, throwable = throwable) + + @Suppress("UNCHECKED_CAST") + fun asType() = this as Result + } +} diff --git a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt new file mode 100644 index 000000000000..92d752c83a9f --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.util.Patterns +import com.android.statementservice.retriever.Relation +import java.net.URL +import java.security.MessageDigest + +internal object StatementUtils { + + /** + * Field name for namespace. + */ + const val NAMESPACE_FIELD = "namespace" + + /** + * Supported asset namespaces. + */ + const val NAMESPACE_WEB = "web" + const val NAMESPACE_ANDROID_APP = "android_app" + + /** + * Field names in a web asset descriptor. + */ + const val WEB_ASSET_FIELD_SITE = "site" + + /** + * Field names in a Android app asset descriptor. + */ + const val ANDROID_APP_ASSET_FIELD_PACKAGE_NAME = "package_name" + const val ANDROID_APP_ASSET_FIELD_CERT_FPS = "sha256_cert_fingerprints" + + /** + * Field names in a statement. + */ + const val ASSET_DESCRIPTOR_FIELD_RELATION = "relation" + const val ASSET_DESCRIPTOR_FIELD_TARGET = "target" + const val DELEGATE_FIELD_DELEGATE = "include" + + val HEX_DIGITS = + charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + + val RELATION by lazy { Relation.create("delegate_permission/common.handle_all_urls") } + private const val ANDROID_ASSET_FORMAT = + """{"namespace": "android_app", "package_name": "%s", "sha256_cert_fingerprints": [%s]}""" + private const val WEB_ASSET_FORMAT = """{"namespace": "web", "site": "%s"}""" + + private val digesterSha256 by lazy { tryOrNull { MessageDigest.getInstance("SHA-256") } } + + internal inline fun tryOrNull(block: () -> T) = + try { + block() + } catch (ignored: Exception) { + null + } + + /** + * Returns the normalized sha-256 fingerprints of a given package according to the Android + * package manager. + */ + fun getCertFingerprintsFromPackageManager( + context: Context, + packageName: String + ): Result> { + val signingInfo = try { + context.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.MATCH_ANY_USER + ) + .signingInfo + } catch (e: Exception) { + return Result.Failure(e) + } + return if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + signingInfo.signingCertificateHistory + }.map { + val result = computeNormalizedSha256Fingerprint(it.toByteArray()) + if (result is Result.Failure) { + return result.asType() + } else { + (result as Result.Success).value + } + }.let { Result.Success(it) } + } + + /** + * Computes the hash of the byte array using the specified algorithm, returning a hex string + * with a colon between each byte. + */ + fun computeNormalizedSha256Fingerprint(signature: ByteArray) = + digesterSha256?.digest(signature) + ?.let(StatementUtils::bytesToHexString) + ?.let { Result.Success(it) } + ?: Result.Failure() + + private fun bytesToHexString(bytes: ByteArray): String { + val hexChars = CharArray(bytes.size * 3 - 1) + var bufIndex = 0 + for (index in bytes.indices) { + val byte = bytes[index].toInt() and 0xFF + if (index > 0) { + hexChars[bufIndex++] = ':' + } + + hexChars[bufIndex++] = HEX_DIGITS[byte ushr 4] + hexChars[bufIndex++] = HEX_DIGITS[byte and 0x0F] + } + return String(hexChars) + } + + fun createAndroidAssetString(context: Context, packageName: String): Result { + val result = getCertFingerprintsFromPackageManager(context, packageName) + if (result is Result.Failure) { + return result.asType() + } + return Result.Success( + ANDROID_ASSET_FORMAT.format( + packageName, + (result as Result.Success).value.joinToString(separator = "\", \"") + ) + ) + } + + fun createAndroidAsset(packageName: String, certFingerprints: List) = + String.format( + ANDROID_ASSET_FORMAT, + packageName, + certFingerprints.joinToString(separator = ", ") { "\"$it\"" }) + + fun createWebAssetString(scheme: String, host: String): Result { + if (!Patterns.DOMAIN_NAME.matcher(host).matches()) { + return Result.Failure("Input host is not valid.") + } + if (scheme != "http" && scheme != "https") { + return Result.Failure("Input scheme is not valid.") + } + return Result.Success(WEB_ASSET_FORMAT.format(URL(scheme, host, "").toString())) + } + + // Hosts with *. for wildcard subdomain support are verified against their root domain + fun createWebAssetString(host: String) = + WEB_ASSET_FORMAT.format(URL("https", host.removePrefix("*."), "").toString()) +} diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2f8ba6da89a3..5f4b22c61250 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -8167,7 +8167,7 @@ public class PackageManagerService extends IPackageManager.Stub } if (best == null || cur.priority > best.priority) { - if (cur.getComponentInfo().enabled) { + if (isComponentEffectivelyEnabled(cur.getComponentInfo(), UserHandle.USER_SYSTEM)) { best = cur; } else { Slog.w(TAG, "Domain verification agent found but not enabled"); @@ -24107,6 +24107,42 @@ public class PackageManagerService extends IPackageManager.Stub } } + /** + * @return true if the runtime app user enabled state, runtime component user enabled state, + * install-time app manifest enabled state, and install-time component manifest enabled state + * are all effectively enabled for the given component. Or if the component cannot be found, + * returns false. + */ + private boolean isComponentEffectivelyEnabled(@NonNull ComponentInfo componentInfo, + @UserIdInt int userId) { + synchronized (mLock) { + try { + String packageName = componentInfo.packageName; + int appEnabledSetting = + mSettings.getApplicationEnabledSettingLPr(packageName, userId); + if (appEnabledSetting == COMPONENT_ENABLED_STATE_DEFAULT) { + if (!componentInfo.applicationInfo.enabled) { + return false; + } + } else if (appEnabledSetting != COMPONENT_ENABLED_STATE_ENABLED) { + return false; + } + + int componentEnabledSetting = mSettings.getComponentEnabledSettingLPr( + componentInfo.getComponentName(), userId); + if (componentEnabledSetting == COMPONENT_ENABLED_STATE_DEFAULT) { + return componentInfo.isEnabled(); + } else if (componentEnabledSetting != COMPONENT_ENABLED_STATE_ENABLED) { + return false; + } + + return true; + } catch (PackageManager.NameNotFoundException ignored) { + return false; + } + } + } + @Override public void enterSafeMode() { enforceSystemOrRoot("Only the system can request entering safe mode");