Merge "Update com.android.statementservice to support v2 API" into sc-dev am: 53c5d85bef
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13438575 Change-Id: I93820ba5fc99985d63b88bd7893efcd65dc0b575
This commit is contained in:
@@ -497,6 +497,8 @@ applications that come with the platform
|
|||||||
|
|
||||||
<privapp-permissions package="com.android.statementservice">
|
<privapp-permissions package="com.android.statementservice">
|
||||||
<permission name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
|
<permission name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
|
||||||
|
<permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
|
||||||
|
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
|
||||||
</privapp-permissions>
|
</privapp-permissions>
|
||||||
|
|
||||||
<privapp-permissions package="com.android.traceur">
|
<privapp-permissions package="com.android.traceur">
|
||||||
|
|||||||
@@ -22,17 +22,24 @@ package {
|
|||||||
|
|
||||||
android_app {
|
android_app {
|
||||||
name: "StatementService",
|
name: "StatementService",
|
||||||
defaults: ["platform_app_defaults"],
|
// Removed because Errorprone doesn't work with Kotlin, can fix up in the future
|
||||||
srcs: ["src/**/*.java"],
|
// defaults: ["platform_app_defaults"],
|
||||||
|
srcs: [
|
||||||
|
"src/**/*.java",
|
||||||
|
"src/**/*.kt",
|
||||||
|
],
|
||||||
optimize: {
|
optimize: {
|
||||||
proguard_flags_files: ["proguard.flags"],
|
proguard_flags_files: ["proguard.flags"],
|
||||||
},
|
},
|
||||||
|
target_sdk_version: "29",
|
||||||
platform_apis: true,
|
platform_apis: true,
|
||||||
privileged: true,
|
privileged: true,
|
||||||
libs: ["org.apache.http.legacy"],
|
certificate: "platform",
|
||||||
uses_libs: ["org.apache.http.legacy"],
|
|
||||||
static_libs: [
|
static_libs: [
|
||||||
"libprotobuf-java-nano",
|
"androidx.appcompat_appcompat",
|
||||||
"volley",
|
"androidx.collection_collection-ktx",
|
||||||
|
"androidx.work_work-runtime",
|
||||||
|
"androidx.work_work-runtime-ktx",
|
||||||
|
"kotlinx-coroutines-android",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,41 +14,62 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest
|
||||||
package="com.android.statementservice"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:versionCode="1"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:versionName="1.0">
|
package="com.android.statementservice"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
|
||||||
<uses-permission android:name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
|
<uses-permission android:name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/service_name"
|
android:label="@string/service_name"
|
||||||
android:allowBackup="false">
|
android:allowBackup="false"
|
||||||
<uses-library android:name="org.apache.http.legacy" />
|
android:name=".StatementServiceApplication"
|
||||||
<service
|
>
|
||||||
android:name=".DirectStatementService"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<action android:name="com.android.statementservice.aosp.service.CHECK_ACTION"/>
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".IntentFilterVerificationReceiver"
|
android:name=".domain.BootCompletedReceiver"
|
||||||
android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER"
|
android:exported="true">
|
||||||
android:exported="true">
|
<intent-filter>
|
||||||
<!-- Set the priority 1 so newer implementation can have higher priority. -->
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
<intent-filter
|
</intent-filter>
|
||||||
android:priority="1">
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".domain.DomainVerificationReceiverV1"
|
||||||
|
android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER"
|
||||||
|
android:exported="true"
|
||||||
|
>
|
||||||
|
<intent-filter android:priority="1">
|
||||||
<action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
|
<action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
|
||||||
<data android:mimeType="application/vnd.android.package-archive"/>
|
<data android:mimeType="application/vnd.android.package-archive"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
v2 receiver remains disabled assuming the device ships its own updated version.
|
||||||
|
If necessary, this can be enabled using shell.
|
||||||
|
-->
|
||||||
|
<receiver
|
||||||
|
android:name=".domain.DomainVerificationReceiverV2"
|
||||||
|
android:permission="android.permission.BIND_DOMAIN_VERIFICATION_AGENT"
|
||||||
|
android:directBootAware="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="false"
|
||||||
|
>
|
||||||
|
<intent-filter android:priority="1">
|
||||||
|
<action android:name="android.intent.action.DOMAINS_NEED_VERIFICATION"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -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.
|
|
||||||
*
|
|
||||||
* <p>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}.
|
|
||||||
*
|
|
||||||
* <p>A relation string.
|
|
||||||
*/
|
|
||||||
public static final String EXTRA_RELATION =
|
|
||||||
"com.android.statementservice.service.RELATION";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameter for {@link #CHECK_ALL_ACTION}.
|
|
||||||
*
|
|
||||||
* <p>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}.
|
|
||||||
*
|
|
||||||
* <p>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}.
|
|
||||||
*
|
|
||||||
* <p>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<String> 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<Void>(
|
|
||||||
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<Void> {
|
|
||||||
|
|
||||||
private List<String> mSources;
|
|
||||||
private String mTarget;
|
|
||||||
private String mRelation;
|
|
||||||
private ResultReceiver mResultReceiver;
|
|
||||||
|
|
||||||
public IsAssociatedCallable(List<String> 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<String> failedSources = new ArrayList<String>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<V> extends FutureTask<V> {
|
|
||||||
|
|
||||||
private final String mTag;
|
|
||||||
|
|
||||||
public ExceptionLoggingFutureTask(Callable<V> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.<String>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<String> finalHosts = new ArrayList<String>(hostList.length);
|
|
||||||
try {
|
|
||||||
ArrayList<String> sourceAssets = new ArrayList<String>();
|
|
||||||
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<String> 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<String> 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.<String>emptyList());
|
|
||||||
} else {
|
|
||||||
mPackageManager.verifyIntentFilter(mVerificationId,
|
|
||||||
PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
|
|
||||||
resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendErrorToPackageManager(mPackageManager, mVerificationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RetryRequestWorker>()
|
||||||
|
.setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<DomainVerificationRequest>(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RetryRequestWorker>(Duration.ofHours(PERIODIC_SHORT_HOURS))
|
||||||
|
.setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.setRequiresDeviceIdle(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.let {
|
||||||
|
enqueueUniquePeriodicWork(
|
||||||
|
PERIODIC_SHORT_ID,
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP, it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PeriodicWorkRequestBuilder<RetryRequestWorker>(Duration.ofDays(PERIODIC_LONG_HOURS))
|
||||||
|
.setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.setRequiresDeviceIdle(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.let {
|
||||||
|
enqueueUniquePeriodicWork(
|
||||||
|
PERIODIC_LONG_ID,
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP, it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String>): Iterable<Triple<UUID, String, String>> {
|
||||||
|
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<WorkResult, VerifyStatus> {
|
||||||
|
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<WorkResult, VerifyStatus> {
|
||||||
|
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<String, Optional<AbstractAssetMatcher>>(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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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<CollectV1Worker>()
|
||||||
|
.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<String>()
|
||||||
|
val failedHosts = mutableListOf<String>()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SingleV1RequestWorker>()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SingleV2RequestWorker>()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ParsedStatement> {
|
||||||
|
val statements: MutableList<Statement> = ArrayList()
|
||||||
|
val delegates: MutableList<String> = 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<ParsedStatement> {
|
||||||
|
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<Statement>, val delegates: List<String>)
|
||||||
|
}
|
||||||
@@ -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<Statement>,
|
||||||
|
val responseCode: Int?
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = Result(emptyList(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(statements: List<Statement>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -81,6 +81,9 @@ public abstract class AbstractAsset {
|
|||||||
/**
|
/**
|
||||||
* If this is the source asset of a statement file, should the retriever follow
|
* If this is the source asset of a statement file, should the retriever follow
|
||||||
* any insecure (non-HTTPS) include statements made by the asset.
|
* 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();
|
public abstract boolean followInsecureInclude();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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".
|
|
||||||
* <p>
|
|
||||||
* A statement S(r, a, b) is considered <b>reliable</b> 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.
|
|
||||||
* <p>
|
|
||||||
* For example, to get the statements made by www.example.com use:
|
|
||||||
* <pre>
|
|
||||||
* result = retrieveStatements(AssetFactory.create(
|
|
||||||
* "{\"namespace\": \"web\", \"site\": \"https://www.google.com\"}"))
|
|
||||||
* </pre>
|
|
||||||
* {@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<Statement> getStatements();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the expiration time in millisecond.
|
|
||||||
*/
|
|
||||||
public long getExpireMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new StatementRetriever that directly retrieves statements from the asset.
|
|
||||||
*
|
|
||||||
* <p> 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.
|
|
||||||
*
|
|
||||||
* <p> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package com.android.statementservice.retriever;
|
package com.android.statementservice.retriever;
|
||||||
|
|
||||||
|
import com.android.statementservice.utils.StatementUtils;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -34,7 +36,8 @@ import java.util.Locale;
|
|||||||
* "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
|
* "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
|
||||||
*
|
*
|
||||||
* <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp",
|
* <p>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"]
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
|
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
|
||||||
@@ -43,7 +46,7 @@ import java.util.Locale;
|
|||||||
* <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
|
* <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
|
||||||
* representing the certificate SHA-256 fingerprint.
|
* 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_FIELD_FORMAT_STRING = "Expected %s to be set.";
|
||||||
private static final String MISSING_APPCERTS_FORMAT_STRING =
|
private static final String MISSING_APPCERTS_FORMAT_STRING =
|
||||||
@@ -65,9 +68,10 @@ import java.util.Locale;
|
|||||||
public String toJson() {
|
public String toJson() {
|
||||||
AssetJsonWriter writer = new AssetJsonWriter();
|
AssetJsonWriter writer = new AssetJsonWriter();
|
||||||
|
|
||||||
writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
|
writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD,
|
||||||
writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
|
StatementUtils.NAMESPACE_ANDROID_APP);
|
||||||
writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
|
writer.writeFieldLower(StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
|
||||||
|
writer.writeArrayUpper(StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
|
||||||
|
|
||||||
return writer.closeAndGetString();
|
return writer.closeAndGetString();
|
||||||
}
|
}
|
||||||
@@ -114,17 +118,17 @@ import java.util.Locale;
|
|||||||
*/
|
*/
|
||||||
public static AndroidAppAsset create(JSONObject asset)
|
public static AndroidAppAsset create(JSONObject asset)
|
||||||
throws AssociationServiceException {
|
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("")) {
|
if (packageName.equals("")) {
|
||||||
throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
|
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) {
|
if (certArray == null || certArray.length() == 0) {
|
||||||
throw new AssociationServiceException(
|
throw new AssociationServiceException(
|
||||||
String.format(MISSING_APPCERTS_FORMAT_STRING,
|
String.format(MISSING_APPCERTS_FORMAT_STRING,
|
||||||
Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
|
StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
|
||||||
}
|
}
|
||||||
List<String> certFingerprints = new ArrayList<>(certArray.length());
|
List<String> certFingerprints = new ArrayList<>(certArray.length());
|
||||||
for (int i = 0; i < certArray.length(); i++) {
|
for (int i = 0; i < certArray.length(); i++) {
|
||||||
@@ -133,7 +137,7 @@ import java.util.Locale;
|
|||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
throw new AssociationServiceException(
|
throw new AssociationServiceException(
|
||||||
String.format(APPCERT_NOT_STRING_FORMAT_STRING,
|
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.
|
* 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
|
* @param certFingerprints at least one of the Android app signing certificate sha-256
|
||||||
* fingerprint.
|
* fingerprint.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.util.Set;
|
|||||||
* Match assets that have the same 'package_name' field and have at least one common certificate
|
* Match assets that have the same 'package_name' field and have at least one common certificate
|
||||||
* fingerprint in 'sha256_cert_fingerprints' field.
|
* fingerprint in 'sha256_cert_fingerprints' field.
|
||||||
*/
|
*/
|
||||||
/* package private */ final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
|
public final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
|
||||||
|
|
||||||
private final AndroidAppAsset mQuery;
|
private final AndroidAppAsset mQuery;
|
||||||
|
|
||||||
|
|||||||
@@ -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())}.
|
|
||||||
*
|
|
||||||
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: {@code
|
|
||||||
* keytool -list -printcert -jarfile signed_app.apk}
|
|
||||||
*
|
|
||||||
* <p>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<String> 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<String> 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.<String>emptyList();
|
|
||||||
}
|
|
||||||
int tokenResourceId = appInfo.metaData.getInt(ASSOCIATED_ASSETS_KEY);
|
|
||||||
if (tokenResourceId == 0) {
|
|
||||||
return Collections.<String>emptyList();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Arrays.asList(
|
|
||||||
mContext.getPackageManager().getResourcesForApplication(packageName)
|
|
||||||
.getStringArray(tokenResourceId));
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
return Collections.<String>emptyList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,12 +16,14 @@
|
|||||||
|
|
||||||
package com.android.statementservice.retriever;
|
package com.android.statementservice.retriever;
|
||||||
|
|
||||||
|
import com.android.statementservice.utils.StatementUtils;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory to create asset from JSON string.
|
* 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.";
|
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)
|
public static AbstractAsset create(JSONObject asset)
|
||||||
throws AssociationServiceException {
|
throws AssociationServiceException {
|
||||||
String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
|
String namespace = asset.optString(StatementUtils.NAMESPACE_FIELD, null);
|
||||||
if (namespace == null) {
|
if (namespace == null) {
|
||||||
throw new AssociationServiceException(String.format(
|
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);
|
return WebAsset.create(asset);
|
||||||
} else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
|
} else if (namespace.equals(StatementUtils.NAMESPACE_ANDROID_APP)) {
|
||||||
return AndroidAppAsset.create(asset);
|
return AndroidAppAsset.create(asset);
|
||||||
} else {
|
} else {
|
||||||
throw new AssociationServiceException("Namespace " + namespace + " is not supported.");
|
throw new AssociationServiceException("Namespace " + namespace + " is not supported.");
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package com.android.statementservice.retriever;
|
package com.android.statementservice.retriever;
|
||||||
|
|
||||||
|
import com.android.statementservice.utils.StatementUtils;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -31,15 +33,15 @@ import org.json.JSONObject;
|
|||||||
JSONException {
|
JSONException {
|
||||||
JSONObject queryObject = new JSONObject(query);
|
JSONObject queryObject = new JSONObject(query);
|
||||||
|
|
||||||
String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null);
|
String namespace = queryObject.optString(StatementUtils.NAMESPACE_FIELD, null);
|
||||||
if (namespace == null) {
|
if (namespace == null) {
|
||||||
throw new AssociationServiceException(String.format(
|
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));
|
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));
|
return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject));
|
||||||
} else {
|
} else {
|
||||||
throw new AssociationServiceException(
|
throw new AssociationServiceException(
|
||||||
|
|||||||
@@ -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<Statement> mStatements;
|
|
||||||
private final Long mExpireMillis;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Statement> getStatements() {
|
|
||||||
return mStatements;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getExpireMillis() {
|
|
||||||
return mExpireMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result(List<Statement> statements, Long expireMillis) {
|
|
||||||
mStatements = statements;
|
|
||||||
mExpireMillis = expireMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Result create(List<Statement> 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<Statement> statements = new ArrayList<Statement>();
|
|
||||||
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<String> delegates = new ArrayList<String>();
|
|
||||||
List<Statement> statements = new ArrayList<Statement>();
|
|
||||||
|
|
||||||
List<String> 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.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Statement> mStatements;
|
|
||||||
private final List<String> mDelegates;
|
|
||||||
|
|
||||||
public ParsedStatement(List<Statement> statements, List<String> delegates) {
|
|
||||||
this.mStatements = statements;
|
|
||||||
this.mDelegates = delegates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Statement> getStatements() {
|
|
||||||
return mStatements;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getDelegates() {
|
|
||||||
return mDelegates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,11 @@
|
|||||||
package com.android.statementservice.retriever;
|
package com.android.statementservice.retriever;
|
||||||
|
|
||||||
import android.annotation.NonNull;
|
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.
|
* An immutable value type representing a statement, consisting of a source, target, and relation.
|
||||||
@@ -31,9 +36,9 @@ import android.annotation.NonNull;
|
|||||||
* }
|
* }
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a
|
* Then invoking {@link StatementRetriever#retrieve(AbstractAsset, Network, Continuation)} will
|
||||||
* {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation}
|
* return a {@link Statement} with {@link #getSource} equal to the input parameter,
|
||||||
* equal to
|
* {@link #getRelation} equal to
|
||||||
*
|
*
|
||||||
* <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
|
* <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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<Statement> statements = new ArrayList<Statement>();
|
|
||||||
List<String> delegates = new ArrayList<String>();
|
|
||||||
|
|
||||||
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<Statement> statements = new ArrayList<Statement>();
|
|
||||||
List<String> delegates = new ArrayList<String>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* 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<String, List<String>> headers) {
|
|
||||||
if (headers == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> 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<String, String> joinHttpHeaders(Map<String, List<String>> headers) {
|
|
||||||
Map<String, String> joinedHeaders = new HashMap<String, String>();
|
|
||||||
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
|
|
||||||
List<String> values = entry.getValue();
|
|
||||||
if (values.size() == 1) {
|
|
||||||
joinedHeaders.put(entry.getKey(), values.get(0));
|
|
||||||
} else {
|
|
||||||
joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return joinedHeaders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> 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<String> getCertFingerprintsFromPackageManager(String packageName,
|
|
||||||
Context context) throws NameNotFoundException {
|
|
||||||
Signature[] signatures = context.getPackageManager().getPackageInfo(packageName,
|
|
||||||
PackageManager.GET_SIGNATURES).signatures;
|
|
||||||
ArrayList<String> result = new ArrayList<String>(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<String> list1, List<String> list2) {
|
|
||||||
HashSet<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package com.android.statementservice.retriever;
|
package com.android.statementservice.retriever;
|
||||||
|
|
||||||
|
import com.android.statementservice.utils.StatementUtils;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@@ -36,7 +38,7 @@ import java.util.Locale;
|
|||||||
* <p>The only protocol supported now are https and http. If the optional port is not specified,
|
* <p>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).
|
* 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 MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
|
||||||
private static final String SCHEME_HTTP = "http";
|
private static final String SCHEME_HTTP = "http";
|
||||||
@@ -73,8 +75,8 @@ import java.util.Locale;
|
|||||||
public String toJson() {
|
public String toJson() {
|
||||||
AssetJsonWriter writer = new AssetJsonWriter();
|
AssetJsonWriter writer = new AssetJsonWriter();
|
||||||
|
|
||||||
writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB);
|
writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD, StatementUtils.NAMESPACE_WEB);
|
||||||
writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
|
writer.writeFieldLower(StatementUtils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
|
||||||
|
|
||||||
return writer.closeAndGetString();
|
return writer.closeAndGetString();
|
||||||
}
|
}
|
||||||
@@ -119,14 +121,14 @@ import java.util.Locale;
|
|||||||
*/
|
*/
|
||||||
protected static WebAsset create(JSONObject asset)
|
protected static WebAsset create(JSONObject asset)
|
||||||
throws AssociationServiceException {
|
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,
|
throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
|
||||||
Utils.WEB_ASSET_FIELD_SITE));
|
StatementUtils.WEB_ASSET_FIELD_SITE));
|
||||||
}
|
}
|
||||||
|
|
||||||
URL url;
|
URL url;
|
||||||
try {
|
try {
|
||||||
url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE));
|
url = new URL(asset.optString(StatementUtils.WEB_ASSET_FIELD_SITE));
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new AssociationServiceException("Url is not well formatted.", e);
|
throw new AssociationServiceException("Url is not well formatted.", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ public final class WebContent {
|
|||||||
|
|
||||||
private final String mContent;
|
private final String mContent;
|
||||||
private final Long mExpireTimeMillis;
|
private final Long mExpireTimeMillis;
|
||||||
|
private final int mResponseCode;
|
||||||
|
|
||||||
public WebContent(String content, Long expireTimeMillis) {
|
public WebContent(String content, Long expireTimeMillis, int responseCode) {
|
||||||
mContent = content;
|
mContent = content;
|
||||||
mExpireTimeMillis = expireTimeMillis;
|
mExpireTimeMillis = expireTimeMillis;
|
||||||
|
mResponseCode = responseCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,4 +48,8 @@ public final class WebContent {
|
|||||||
public String getContent() {
|
public String getContent() {
|
||||||
return mContent;
|
return mContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getResponseCode() {
|
||||||
|
return mResponseCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T> {
|
||||||
|
|
||||||
|
fun successValueOrNull() = (this as? Success<T>)?.value
|
||||||
|
|
||||||
|
data class Success<T>(val value: T) : Result<T>()
|
||||||
|
data class Failure<T>(val message: String? = null, val throwable: Throwable? = null) :
|
||||||
|
Result<T>() {
|
||||||
|
|
||||||
|
constructor(message: String) : this(message = message, throwable = null)
|
||||||
|
constructor(throwable: Throwable) : this(message = null, throwable = throwable)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T> asType() = this as Result<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <T> 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<List<String>> {
|
||||||
|
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<String> {
|
||||||
|
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>) =
|
||||||
|
String.format(
|
||||||
|
ANDROID_ASSET_FORMAT,
|
||||||
|
packageName,
|
||||||
|
certFingerprints.joinToString(separator = ", ") { "\"$it\"" })
|
||||||
|
|
||||||
|
fun createWebAssetString(scheme: String, host: String): Result<String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -8167,7 +8167,7 @@ public class PackageManagerService extends IPackageManager.Stub
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (best == null || cur.priority > best.priority) {
|
if (best == null || cur.priority > best.priority) {
|
||||||
if (cur.getComponentInfo().enabled) {
|
if (isComponentEffectivelyEnabled(cur.getComponentInfo(), UserHandle.USER_SYSTEM)) {
|
||||||
best = cur;
|
best = cur;
|
||||||
} else {
|
} else {
|
||||||
Slog.w(TAG, "Domain verification agent found but not enabled");
|
Slog.w(TAG, "Domain verification agent found but not enabled");
|
||||||
@@ -24113,6 +24113,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
|
@Override
|
||||||
public void enterSafeMode() {
|
public void enterSafeMode() {
|
||||||
enforceSystemOrRoot("Only the system can request entering safe mode");
|
enforceSystemOrRoot("Only the system can request entering safe mode");
|
||||||
|
|||||||
Reference in New Issue
Block a user