Implement IntentFilter verification service.
This commit adds a verifier that verifies a host delegates permission for an app to handle Url for the host using the Statement protocol. - Implements the Statement protocol -- The protocol defines a file format that represents statements. -- The protocol defines where each asset type should put their statement declaration. For web asset, the statement file should be hosted at <scheme>://<host>:<port>/.well-known/associations.json. - Implements IntentFilterVerificationReceiver, an interface between StatementService and PackageManager. PackageManager will send a broadcast with action Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION. The service will process the request and returns the results by calling PackageManager.verifyIntentFilter(). To verify an IntentFilter like this defined in Android app com.test.app <intent-filter> <data android:scheme="https" /> <data android:host="www.test.com" /> <data android:pathPattern=".*"/> </intent-filter> The service will try to retrieve the statement file from https://www.test.com:443/.well-known/associations.json and try to find a JSON object equivalent to {'relation': ['delegate_permission/common.handle_all_urls'], 'target': {'namespace': 'android_app', 'package_name': 'com.test.app', 'sha256_cert_fingerprints': [APP_CERT_FP]}} The entry should have the correct relation, package name, and certificate sha256 fingerprint. Because this implementation will send a HTTP request for each host specified in the intent-filter in AndroidManifest.xml, to avoid overwhelming the network at app install time, we limit the maximum number of hosts we will verify for a single app to 10. Any app with more than 10 hosts in the autoVerify=true intent-filter won't be auto verified. Change-Id: I787c9d176e4110aa441eb5fe4fa9651a071c6610
This commit is contained in:
33
packages/StatementService/Android.mk
Normal file
33
packages/StatementService/Android.mk
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
|
||||
|
||||
LOCAL_PACKAGE_NAME := StatementService
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := \
|
||||
libprotobuf-java-nano \
|
||||
volley
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
include $(call all-makefiles-under,$(LOCAL_PATH)/src)
|
||||
51
packages/StatementService/AndroidManifest.xml
Normal file
51
packages/StatementService/AndroidManifest.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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.INTENT_FILTER_VERIFICATION_AGENT"/>
|
||||
|
||||
<application
|
||||
android:label="@string/service_name"
|
||||
android:allowBackup="false">
|
||||
<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
|
||||
android:name=".IntentFilterVerificationReceiver"
|
||||
android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER">
|
||||
<!-- Set the priority 1 so newer implementation can have higher priority. -->
|
||||
<intent-filter
|
||||
android:priority="1">
|
||||
<action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
|
||||
<data android:mimeType="application/vnd.android.package-archive"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
0
packages/StatementService/proguard.flags
Normal file
0
packages/StatementService/proguard.flags
Normal file
19
packages/StatementService/res/values/strings.xml
Normal file
19
packages/StatementService/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="service_name">Intent Filter Verification Service</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* 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();
|
||||
if (mThread != null) {
|
||||
mThread.quit();
|
||||
}
|
||||
|
||||
try {
|
||||
if (mHttpResponseCache != null) {
|
||||
mHttpResponseCache.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "HTTP(S) response cache deletion failed:" + e);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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.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);
|
||||
|
||||
Log.i(TAG, "Verify IntentFilter for " + hosts);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
ArrayList<String> sourceAssets = new ArrayList<String>();
|
||||
for (String host : hostList) {
|
||||
sourceAssets.add(createWebAssetString(scheme, 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));
|
||||
|
||||
serviceIntent.putExtras(extras);
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Intent action not supported: " + action);
|
||||
}
|
||||
}
|
||||
|
||||
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,66 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A handle representing the identity and address of some digital asset. An asset is an online
|
||||
* entity that typically provides some service or content. Examples of assets are websites, Android
|
||||
* apps, Twitter feeds, and Plus Pages.
|
||||
*
|
||||
* <p> Asset can be represented by a JSON string. For example, the web site https://www.google.com
|
||||
* can be represented by
|
||||
* <pre>
|
||||
* {"namespace": "web", "site": "https://www.google.com"}
|
||||
* </pre>
|
||||
*
|
||||
* <p> The Android app with package name com.google.test that is signed by a certificate with sha256
|
||||
* fingerprint 11:22:33 can be represented by
|
||||
* <pre>
|
||||
* {"namespace": "android_app",
|
||||
* "package_name": "com.google.test",
|
||||
* "sha256_cert_fingerprints": ["11:22:33"]}
|
||||
* </pre>
|
||||
*
|
||||
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
|
||||
* {@code keytool -list -printcert -jarfile signed_app.apk}
|
||||
*/
|
||||
public abstract class AbstractAsset {
|
||||
|
||||
/**
|
||||
* Returns a JSON string representation of this asset. The strings returned by this function are
|
||||
* normalized -- they can be used for equality testing.
|
||||
*/
|
||||
public abstract String toJson();
|
||||
|
||||
/**
|
||||
* Returns a key that can be used by {@link AbstractAssetMatcher} to lookup the asset.
|
||||
*
|
||||
* <p> An asset will match an {@code AssetMatcher} only if the value of this method is equal to
|
||||
* {@code AssetMatcher.getMatchedLookupKey()}.
|
||||
*/
|
||||
public abstract int lookupKey();
|
||||
|
||||
/**
|
||||
* Creates a new Asset from its JSON string representation.
|
||||
*
|
||||
* @throws AssociationServiceException if the assetJson is not well formatted.
|
||||
*/
|
||||
public static AbstractAsset create(String assetJson)
|
||||
throws AssociationServiceException {
|
||||
return AssetFactory.create(assetJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 org.json.JSONException;
|
||||
|
||||
/**
|
||||
* An asset matcher that can match asset with the given query.
|
||||
*/
|
||||
public abstract class AbstractAssetMatcher {
|
||||
|
||||
/**
|
||||
* Returns true if this AssetMatcher matches the asset.
|
||||
*/
|
||||
public abstract boolean matches(AbstractAsset asset);
|
||||
|
||||
/**
|
||||
* This AssetMatcher will only match Asset with {@code lookupKey()} equal to the value returned
|
||||
* by this method.
|
||||
*/
|
||||
public abstract int getMatchedLookupKey();
|
||||
|
||||
/**
|
||||
* Creates a new AssetMatcher from its JSON string representation.
|
||||
*
|
||||
* <p> For web namespace, {@code query} will match assets that have the same 'site' field.
|
||||
*
|
||||
* <p> For Android namespace, {@code query} will match assets that have the same
|
||||
* 'package_name' field and have at least one common certificate fingerprint in
|
||||
* 'sha256_cert_fingerprints' field.
|
||||
*/
|
||||
public static AbstractAssetMatcher createMatcher(String query)
|
||||
throws AssociationServiceException, JSONException {
|
||||
return AssetMatcherFactory.create(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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/associations.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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Immutable value type that names an Android app asset.
|
||||
*
|
||||
* <p>An Android app can be named by its package name and certificate fingerprints using this JSON
|
||||
* string: { "namespace": "android_app", "package_name": "[Java package name]",
|
||||
* "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
|
||||
*
|
||||
* <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"]
|
||||
* }
|
||||
*
|
||||
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
|
||||
* {@code keytool -list -printcert -jarfile signed_app.apk}
|
||||
*
|
||||
* <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
|
||||
* representing the certificate SHA-256 fingerprint.
|
||||
*/
|
||||
/* package private */ final class AndroidAppAsset extends AbstractAsset {
|
||||
|
||||
private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
|
||||
private static final String MISSING_APPCERTS_FORMAT_STRING =
|
||||
"Expected %s to be non-empty array.";
|
||||
private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings.";
|
||||
|
||||
private final List<String> mCertFingerprints;
|
||||
private final String mPackageName;
|
||||
|
||||
public List<String> getCertFingerprints() {
|
||||
return Collections.unmodifiableList(mCertFingerprints);
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toJson() {
|
||||
AssetJsonWriter writer = new AssetJsonWriter();
|
||||
|
||||
writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
|
||||
writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
|
||||
writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
|
||||
|
||||
return writer.closeAndGetString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder asset = new StringBuilder();
|
||||
asset.append("AndroidAppAsset: ");
|
||||
asset.append(toJson());
|
||||
return asset.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof AndroidAppAsset)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((AndroidAppAsset) o).toJson().equals(toJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toJson().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lookupKey() {
|
||||
return getPackageName().hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the input is a valid Android app asset.
|
||||
*
|
||||
* @param asset a JSONObject that has "namespace", "package_name", and
|
||||
* "sha256_cert_fingerprints" fields.
|
||||
* @throws AssociationServiceException if the asset is not well formatted.
|
||||
*/
|
||||
public static AndroidAppAsset create(JSONObject asset)
|
||||
throws AssociationServiceException {
|
||||
String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
|
||||
if (packageName.equals("")) {
|
||||
throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
|
||||
Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
|
||||
}
|
||||
|
||||
JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
|
||||
if (certArray == null || certArray.length() == 0) {
|
||||
throw new AssociationServiceException(
|
||||
String.format(MISSING_APPCERTS_FORMAT_STRING,
|
||||
Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
|
||||
}
|
||||
List<String> certFingerprints = new ArrayList<>(certArray.length());
|
||||
for (int i = 0; i < certArray.length(); i++) {
|
||||
try {
|
||||
certFingerprints.add(certArray.getString(i));
|
||||
} catch (JSONException e) {
|
||||
throw new AssociationServiceException(
|
||||
String.format(APPCERT_NOT_STRING_FORMAT_STRING,
|
||||
Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
|
||||
}
|
||||
}
|
||||
|
||||
return new AndroidAppAsset(packageName, certFingerprints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AndroidAppAsset.
|
||||
*
|
||||
* @param packageName the package name of the Android app.
|
||||
* @param certFingerprints at least one of the Android app signing certificate sha-256
|
||||
* fingerprint.
|
||||
*/
|
||||
public static AndroidAppAsset create(String packageName, List<String> certFingerprints) {
|
||||
if (packageName == null || packageName.equals("")) {
|
||||
throw new AssertionError("Expected packageName to be set.");
|
||||
}
|
||||
if (certFingerprints == null || certFingerprints.size() == 0) {
|
||||
throw new AssertionError("Expected certFingerprints to be set.");
|
||||
}
|
||||
List<String> lowerFps = new ArrayList<String>(certFingerprints.size());
|
||||
for (String fp : certFingerprints) {
|
||||
lowerFps.add(fp.toUpperCase(Locale.US));
|
||||
}
|
||||
return new AndroidAppAsset(packageName, lowerFps);
|
||||
}
|
||||
|
||||
private AndroidAppAsset(String packageName, List<String> certFingerprints) {
|
||||
if (packageName.equals("")) {
|
||||
mPackageName = null;
|
||||
} else {
|
||||
mPackageName = packageName;
|
||||
}
|
||||
|
||||
if (certFingerprints == null || certFingerprints.size() == 0) {
|
||||
mCertFingerprints = null;
|
||||
} else {
|
||||
mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ASCII-sorted copy of the list of certs with all duplicates removed.
|
||||
*/
|
||||
private List<String> sortAndDeDuplicate(List<String> certs) {
|
||||
if (certs.size() <= 1) {
|
||||
return certs;
|
||||
}
|
||||
HashSet<String> set = new HashSet<>(certs);
|
||||
List<String> result = new ArrayList<>(set);
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Match assets that have the same 'package_name' field and have at least one common certificate
|
||||
* fingerprint in 'sha256_cert_fingerprints' field.
|
||||
*/
|
||||
/* package private */ final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
|
||||
|
||||
private final AndroidAppAsset mQuery;
|
||||
|
||||
public AndroidAppAssetMatcher(AndroidAppAsset query) {
|
||||
mQuery = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(AbstractAsset asset) {
|
||||
if (asset instanceof AndroidAppAsset) {
|
||||
AndroidAppAsset androidAppAsset = (AndroidAppAsset) asset;
|
||||
if (!androidAppAsset.getPackageName().equals(mQuery.getPackageName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> certs = new HashSet<String>(mQuery.getCertFingerprints());
|
||||
for (String cert : androidAppAsset.getCertFingerprints()) {
|
||||
if (certs.contains(cert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMatchedLookupKey() {
|
||||
return mQuery.lookupKey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Factory to create asset from JSON string.
|
||||
*/
|
||||
/* package private */ final class AssetFactory {
|
||||
|
||||
private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
|
||||
|
||||
private AssetFactory() {}
|
||||
|
||||
/**
|
||||
* Creates a new Asset object from its JSON string representation.
|
||||
*
|
||||
* @throws AssociationServiceException if the assetJson is not well formatted.
|
||||
*/
|
||||
public static AbstractAsset create(String assetJson) throws AssociationServiceException {
|
||||
try {
|
||||
return create(new JSONObject(assetJson));
|
||||
} catch (JSONException e) {
|
||||
throw new AssociationServiceException(
|
||||
"Input is not a well formatted asset descriptor.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the input is a valid asset with purposes.
|
||||
*
|
||||
* @throws AssociationServiceException if the asset is not well formatted.
|
||||
*/
|
||||
private static AbstractAsset create(JSONObject asset)
|
||||
throws AssociationServiceException {
|
||||
String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
|
||||
if (namespace == null) {
|
||||
throw new AssociationServiceException(String.format(
|
||||
FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
|
||||
}
|
||||
|
||||
if (namespace.equals(Utils.NAMESPACE_WEB)) {
|
||||
return WebAsset.create(asset);
|
||||
} else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
|
||||
return AndroidAppAsset.create(asset);
|
||||
} else {
|
||||
throw new AssociationServiceException("Namespace " + namespace + " is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Creates a Json string where the order of the fields can be specified.
|
||||
*/
|
||||
/* package private */ final class AssetJsonWriter {
|
||||
|
||||
private StringWriter mStringWriter = new StringWriter();
|
||||
private JsonWriter mWriter;
|
||||
private boolean mClosed = false;
|
||||
|
||||
public AssetJsonWriter() {
|
||||
mWriter = new JsonWriter(mStringWriter);
|
||||
try {
|
||||
mWriter.beginObject();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Unreachable exception.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a field to the output, putting both the key and value in lowercase. Null values are
|
||||
* not written.
|
||||
*/
|
||||
public void writeFieldLower(String key, String value) {
|
||||
if (mClosed) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot write to an object that has already been closed.");
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
try {
|
||||
mWriter.name(key.toLowerCase(Locale.US));
|
||||
mWriter.value(value.toLowerCase(Locale.US));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Unreachable exception.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an array to the output, putting both the key and values in lowercase. If {@code
|
||||
* values} is null, this field will not be written. Individual values in the list must not be
|
||||
* null.
|
||||
*/
|
||||
public void writeArrayUpper(String key, List<String> values) {
|
||||
if (mClosed) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot write to an object that has already been closed.");
|
||||
}
|
||||
|
||||
if (values != null) {
|
||||
try {
|
||||
mWriter.name(key.toLowerCase(Locale.US));
|
||||
mWriter.beginArray();
|
||||
for (String value : values) {
|
||||
mWriter.value(value.toUpperCase(Locale.US));
|
||||
}
|
||||
mWriter.endArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Unreachable exception.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of the constructed json. After calling this method, {@link
|
||||
* #writeFieldLower} can no longer be called.
|
||||
*/
|
||||
public String closeAndGetString() {
|
||||
if (!mClosed) {
|
||||
try {
|
||||
mWriter.endObject();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Unreachable exception.");
|
||||
}
|
||||
mClosed = true;
|
||||
}
|
||||
return mStringWriter.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Factory to create asset matcher from JSON string.
|
||||
*/
|
||||
/* package private */ final class AssetMatcherFactory {
|
||||
|
||||
private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
|
||||
private static final String NAMESPACE_NOT_SUPPORTED_STRING = "Namespace %s is not supported.";
|
||||
|
||||
public static AbstractAssetMatcher create(String query) throws AssociationServiceException,
|
||||
JSONException {
|
||||
JSONObject queryObject = new JSONObject(query);
|
||||
|
||||
String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null);
|
||||
if (namespace == null) {
|
||||
throw new AssociationServiceException(String.format(
|
||||
FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
|
||||
}
|
||||
|
||||
if (namespace.equals(Utils.NAMESPACE_WEB)) {
|
||||
return new WebAssetMatcher(WebAsset.create(queryObject));
|
||||
} else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
|
||||
return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject));
|
||||
} else {
|
||||
throw new AssociationServiceException(
|
||||
String.format(NAMESPACE_NOT_SUPPORTED_STRING, namespace));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs in the Association Service.
|
||||
*/
|
||||
public class AssociationServiceException extends Exception {
|
||||
|
||||
public AssociationServiceException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public AssociationServiceException(String msg, Exception e) {
|
||||
super(msg, e);
|
||||
}
|
||||
|
||||
public AssociationServiceException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 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/associations.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 url, 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 {
|
||||
webContent = mUrlFetcher.getWebContentFromUrl(new URL(url),
|
||||
HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS);
|
||||
} catch (IOException 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 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 | NameNotFoundException e) {
|
||||
Log.w(DirectStatementRetriever.class.getSimpleName(), e);
|
||||
return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* An immutable value type representing a statement relation with "kind" and "detail".
|
||||
*
|
||||
* <p> The set of kinds is enumerated by the API: <ul> <li> <b>delegate_permission</b>: The detail
|
||||
* field specifies which permission to delegate. A statement involving this relation does not
|
||||
* constitute a requirement to do the delegation, just a permission to do so. </ul>
|
||||
*
|
||||
* <p> We may add other kinds in the future.
|
||||
*
|
||||
* <p> The detail field is a lowercase alphanumeric string with underscores and periods allowed
|
||||
* (matching the regex [a-z0-9_.]+), but otherwise unstructured. It is also possible to specify '*'
|
||||
* (the wildcard character) as the detail if the relation applies to any detail in the specified
|
||||
* kind.
|
||||
*/
|
||||
public final class Relation {
|
||||
|
||||
private static final Pattern KIND_PATTERN = Pattern.compile("^[a-z0-9_.]+$");
|
||||
private static final Pattern DETAIL_PATTERN = Pattern.compile("^([a-z0-9_.]+|[*])$");
|
||||
|
||||
private static final String MATCH_ALL_DETAILS = "*";
|
||||
|
||||
private final String mKind;
|
||||
private final String mDetail;
|
||||
|
||||
private Relation(String kind, String detail) {
|
||||
mKind = kind;
|
||||
mDetail = detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relation's kind.
|
||||
*/
|
||||
@NonNull
|
||||
public String getKind() {
|
||||
return mKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relation's detail.
|
||||
*/
|
||||
@NonNull
|
||||
public String getDetail() {
|
||||
return mDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Relation object for the specified {@code kind} and {@code detail}.
|
||||
*
|
||||
* @throws AssociationServiceException if {@code kind} or {@code detail} is not well formatted.
|
||||
*/
|
||||
public static Relation create(@NonNull String kind, @NonNull String detail)
|
||||
throws AssociationServiceException {
|
||||
if (!KIND_PATTERN.matcher(kind).matches() || !DETAIL_PATTERN.matcher(detail).matches()) {
|
||||
throw new AssociationServiceException("Relation not well formatted.");
|
||||
}
|
||||
return new Relation(kind, detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Relation object from its string representation.
|
||||
*
|
||||
* @throws AssociationServiceException if the relation is not well formatted.
|
||||
*/
|
||||
public static Relation create(@NonNull String relation) throws AssociationServiceException {
|
||||
String[] r = relation.split("/", 2);
|
||||
if (r.length != 2) {
|
||||
throw new AssociationServiceException("Relation not well formatted.");
|
||||
}
|
||||
return create(r[0], r[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code relation} has the same kind and detail. If {@code
|
||||
* relation.getDetail()} is wildcard (*) then returns true if the kind is the same.
|
||||
*/
|
||||
public boolean matches(Relation relation) {
|
||||
return getKind().equals(relation.getKind()) && (getDetail().equals(MATCH_ALL_DETAILS)
|
||||
|| getDetail().equals(relation.getDetail()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this relation.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder relation = new StringBuilder();
|
||||
relation.append(getKind());
|
||||
relation.append("/");
|
||||
relation.append(getDetail());
|
||||
return relation.toString();
|
||||
}
|
||||
|
||||
// equals() and hashCode() are generated by Android Studio.
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Relation relation = (Relation) o;
|
||||
|
||||
if (mDetail != null ? !mDetail.equals(relation.mDetail) : relation.mDetail != null) {
|
||||
return false;
|
||||
}
|
||||
if (mKind != null ? !mKind.equals(relation.mKind) : relation.mKind != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = mKind != null ? mKind.hashCode() : 0;
|
||||
result = 31 * result + (mDetail != null ? mDetail.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* An immutable value type representing a statement, consisting of a source, target, and relation.
|
||||
* This reflects an assertion that the relation holds for the source, target pair. For example, if a
|
||||
* web site has the following in its associations.json file:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "relation": ["delegate_permission/common.handle_all_urls"],
|
||||
* "target" : {"namespace": "android_app", "package_name": "com.example.app",
|
||||
* "sha256_cert_fingerprints": ["00:11:22:33"] }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a
|
||||
* {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation}
|
||||
* equal to
|
||||
*
|
||||
* <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
|
||||
*
|
||||
* and with {@link #getTarget} equal to
|
||||
*
|
||||
* <pre>AbstractAsset.create("{\"namespace\" : \"android_app\","
|
||||
* + "\"package_name\": \"com.example.app\"}"
|
||||
* + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}");
|
||||
* </pre>
|
||||
*/
|
||||
public final class Statement {
|
||||
|
||||
private final AbstractAsset mTarget;
|
||||
private final Relation mRelation;
|
||||
private final AbstractAsset mSource;
|
||||
|
||||
private Statement(AbstractAsset source, AbstractAsset target, Relation relation) {
|
||||
mSource = source;
|
||||
mTarget = target;
|
||||
mRelation = relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source asset of the statement.
|
||||
*/
|
||||
@NonNull
|
||||
public AbstractAsset getSource() {
|
||||
return mSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target asset of the statement.
|
||||
*/
|
||||
@NonNull
|
||||
public AbstractAsset getTarget() {
|
||||
return mTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relation of the statement.
|
||||
*/
|
||||
@NonNull
|
||||
public Relation getRelation() {
|
||||
return mRelation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Statement object for the specified target asset and relation. For example:
|
||||
* <pre>
|
||||
* Asset asset = Asset.Factory.create(
|
||||
* "{\"namespace\" : \"web\",\"site\": \"https://www.test.com\"}");
|
||||
* Relation relation = Relation.create("delegate_permission", "common.get_login_creds");
|
||||
* Statement statement = Statement.create(asset, relation);
|
||||
* </pre>
|
||||
*/
|
||||
public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target,
|
||||
@NonNull Relation relation) {
|
||||
return new Statement(source, target, relation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Statement statement = (Statement) o;
|
||||
|
||||
if (!mRelation.equals(statement.mRelation)) {
|
||||
return false;
|
||||
}
|
||||
if (!mTarget.equals(statement.mTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!mSource.equals(statement.mSource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = mTarget.hashCode();
|
||||
result = 31 * result + mRelation.hashCode();
|
||||
result = 31 * result + mSource.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder statement = new StringBuilder();
|
||||
statement.append("Statement: ");
|
||||
statement.append(mSource);
|
||||
statement.append(", ");
|
||||
statement.append(mTarget);
|
||||
statement.append(", ");
|
||||
statement.append(mRelation);
|
||||
return statement.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class that parses JSON-formatted statements.
|
||||
*/
|
||||
/* package private */ final class StatementParser {
|
||||
|
||||
/**
|
||||
* Parses a JSON array of statements.
|
||||
*/
|
||||
static ParsedStatement parseStatementList(String statementList, AbstractAsset source)
|
||||
throws JSONException, AssociationServiceException {
|
||||
List<Statement> statements = new ArrayList<Statement>();
|
||||
List<String> delegates = new ArrayList<String>();
|
||||
|
||||
JSONArray statementsJson = new JSONArray(statementList);
|
||||
for (int i = 0; i < statementsJson.length(); i++) {
|
||||
ParsedStatement result = parseStatement(statementsJson.getString(i), source);
|
||||
statements.addAll(result.getStatements());
|
||||
delegates.addAll(result.getDelegates());
|
||||
}
|
||||
|
||||
return new ParsedStatement(statements, delegates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single JSON statement.
|
||||
*/
|
||||
static ParsedStatement parseStatement(String statementString, AbstractAsset source)
|
||||
throws JSONException, AssociationServiceException {
|
||||
List<Statement> statements = new ArrayList<Statement>();
|
||||
List<String> delegates = new ArrayList<String>();
|
||||
JSONObject statement = new JSONObject(statementString);
|
||||
if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) {
|
||||
delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE));
|
||||
} else {
|
||||
AbstractAsset target = AssetFactory
|
||||
.create(statement.getString(Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
|
||||
JSONArray relations = statement.getJSONArray(
|
||||
Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
|
||||
for (int i = 0; i < relations.length(); i++) {
|
||||
statements.add(Statement
|
||||
.create(source, target, Relation.create(relations.getString(i))));
|
||||
}
|
||||
}
|
||||
|
||||
return new ParsedStatement(statements, delegates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 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 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.
|
||||
*
|
||||
* @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;
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setConnectTimeout(connectionTimeoutMillis);
|
||||
connection.setReadTimeout(connectionTimeoutMillis);
|
||||
connection.setUseCaches(true);
|
||||
connection.addRequestProperty("Cache-Control", "max-stale=60");
|
||||
|
||||
if (connection.getContentLength() > fileSizeLimit) {
|
||||
throw new AssociationServiceException("The content size of the url is larger than "
|
||||
+ fileSizeLimit);
|
||||
}
|
||||
|
||||
Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader(connection.getHeaderFields());
|
||||
|
||||
try {
|
||||
return new WebContent(inputStreamToString(
|
||||
connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
|
||||
expireTimeMillis);
|
||||
} finally {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 = "delegate";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 org.json.JSONObject;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Immutable value type that names a web asset.
|
||||
*
|
||||
* <p>A web asset can be named by its protocol, domain, and port using this JSON string:
|
||||
* { "namespace": "web",
|
||||
* "site": "[protocol]://[fully-qualified domain]{:[optional port]}" }
|
||||
*
|
||||
* <p>For example, a website hosted on a https server at www.test.com can be named using
|
||||
* { "namespace": "web",
|
||||
* "site": "https://www.test.com" }
|
||||
*
|
||||
* <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).
|
||||
*/
|
||||
/* package private */ final class WebAsset extends AbstractAsset {
|
||||
|
||||
private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
|
||||
|
||||
private final URL mUrl;
|
||||
|
||||
private WebAsset(URL url) {
|
||||
int port = url.getPort() != -1 ? url.getPort() : url.getDefaultPort();
|
||||
try {
|
||||
mUrl = new URL(url.getProtocol().toLowerCase(), url.getHost().toLowerCase(), port, "");
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(
|
||||
"Url should always be validated before calling the constructor.");
|
||||
}
|
||||
}
|
||||
|
||||
public String getDomain() {
|
||||
return mUrl.getHost();
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mUrl.getPath();
|
||||
}
|
||||
|
||||
public String getScheme() {
|
||||
return mUrl.getProtocol();
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return mUrl.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toJson() {
|
||||
AssetJsonWriter writer = new AssetJsonWriter();
|
||||
|
||||
writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB);
|
||||
writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
|
||||
|
||||
return writer.closeAndGetString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder asset = new StringBuilder();
|
||||
asset.append("WebAsset: ");
|
||||
asset.append(toJson());
|
||||
return asset.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof WebAsset)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((WebAsset) o).toJson().equals(toJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toJson().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lookupKey() {
|
||||
return toJson().hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the input is a valid web asset.
|
||||
*
|
||||
* @throws AssociationServiceException if the asset is not well formatted.
|
||||
*/
|
||||
protected static WebAsset create(JSONObject asset)
|
||||
throws AssociationServiceException {
|
||||
if (asset.optString(Utils.WEB_ASSET_FIELD_SITE).equals("")) {
|
||||
throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
|
||||
Utils.WEB_ASSET_FIELD_SITE));
|
||||
}
|
||||
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE));
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssociationServiceException("Url is not well formatted.", e);
|
||||
}
|
||||
|
||||
String scheme = url.getProtocol().toLowerCase(Locale.US);
|
||||
if (!scheme.equals("https") && !scheme.equals("http")) {
|
||||
throw new AssociationServiceException("Expected scheme to be http or https.");
|
||||
}
|
||||
|
||||
if (url.getUserInfo() != null) {
|
||||
throw new AssociationServiceException("The url should not contain user info.");
|
||||
}
|
||||
|
||||
String path = url.getFile(); // This is url.getPath() + url.getQuery().
|
||||
if (!path.equals("/") && !path.equals("")) {
|
||||
throw new AssociationServiceException(
|
||||
"Site should only have scheme, domain, and port.");
|
||||
}
|
||||
|
||||
return new WebAsset(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Match assets that have the same 'site' field.
|
||||
*/
|
||||
/* package private */ final class WebAssetMatcher extends AbstractAssetMatcher {
|
||||
|
||||
private final WebAsset mQuery;
|
||||
|
||||
public WebAssetMatcher(WebAsset query) {
|
||||
mQuery = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(AbstractAsset asset) {
|
||||
if (asset instanceof WebAsset) {
|
||||
WebAsset webAsset = (WebAsset) asset;
|
||||
return webAsset.toJson().equals(mQuery.toJson());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMatchedLookupKey() {
|
||||
return mQuery.lookupKey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An immutable value type representing the response from a web server.
|
||||
*
|
||||
* Visible for testing.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class WebContent {
|
||||
|
||||
private final String mContent;
|
||||
private final Long mExpireTimeMillis;
|
||||
|
||||
public WebContent(String content, Long expireTimeMillis) {
|
||||
mContent = content;
|
||||
mExpireTimeMillis = expireTimeMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration time of the content as specified in the HTTP header.
|
||||
*/
|
||||
public Long getExpireTimeMillis() {
|
||||
return mExpireTimeMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content of the HTTP message body.
|
||||
*/
|
||||
public String getContent() {
|
||||
return mContent;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user