blob: ea95ab54334a191341974d6a59080a81ed8b7de2 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.browserservices;
import android.annotation.SuppressLint;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsService.Relation;
import android.support.v4.util.Pair;
import android.text.TextUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content_public.browser.BrowserStartupController;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Used to verify postMessage origin for a designated package name.
*
* Uses Digital Asset Links to confirm that the given origin is associated with the package name as
* a postMessage origin. It caches any origin that has been verified during the current application
* lifecycle and reuses that without making any new network requests.
*
* The lifecycle of this object is governed by the owner. The owner has to call
* {@link OriginVerifier#cleanUp()} for proper cleanup of dependencies.
*/
@JNINamespace("customtabs")
public class OriginVerifier {
private static final String TAG = "OriginVerifier";
private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();
private static final String USE_AS_ORIGIN = "delegate_permission/common.use_as_origin";
private static final String HANDLE_ALL_URLS = "delegate_permission/common.handle_all_urls";
private static Map<Pair<String, Integer>, Set<Origin>> sPackageToCachedOrigins;
private final OriginVerificationListener mListener;
private final String mPackageName;
private final String mSignatureFingerprint;
private final @Relation int mRelation;
private long mNativeOriginVerifier = 0;
private Origin mOrigin;
/** Small helper class to post a result of origin verification. */
private class VerifiedCallback implements Runnable {
private final boolean mResult;
private final Boolean mOnline;
public VerifiedCallback(boolean result, Boolean online) {
mResult = result;
mOnline = online;
}
@Override
public void run() {
originVerified(mResult, mOnline);
}
}
public static Uri getPostMessageUriFromVerifiedOrigin(String packageName,
Origin verifiedOrigin) {
return Uri.parse(IntentHandler.ANDROID_APP_REFERRER_SCHEME + "://"
+ verifiedOrigin.uri().getHost() + "/" + packageName);
}
/** Clears all known relations. */
@VisibleForTesting
public static void clearCachedVerificationsForTesting() {
ThreadUtils.assertOnUiThread();
if (sPackageToCachedOrigins != null) sPackageToCachedOrigins.clear();
}
/**
* Mark an origin as verified for a package.
* @param packageName The package name to prepopulate for.
* @param origin The origin to add as verified.
* @param relation The Digital Asset Links relation verified.
*/
public static void addVerifiedOriginForPackage(
String packageName, Origin origin, @Relation int relation) {
Log.d(TAG, "Adding: %s for %s", packageName, origin);
ThreadUtils.assertOnUiThread();
if (sPackageToCachedOrigins == null) sPackageToCachedOrigins = new HashMap<>();
Set<Origin> cachedOrigins =
sPackageToCachedOrigins.get(new Pair<>(packageName, relation));
if (cachedOrigins == null) {
cachedOrigins = new HashSet<>();
sPackageToCachedOrigins.put(new Pair<>(packageName, relation), cachedOrigins);
}
cachedOrigins.add(origin);
TrustedWebActivityClient.registerClient(ContextUtils.getApplicationContext(),
origin, packageName);
}
/**
* Returns whether an origin is first-party relative to a given package name.
*
* This only returns data from previously cached relations, and does not
* trigger an asynchronous validation.
*
* @param packageName The package name
* @param origin The origin to verify
* @param relation The Digital Asset Links relation to verify for.
*/
public static boolean isValidOrigin(String packageName, Origin origin, @Relation int relation) {
ThreadUtils.assertOnUiThread();
if (sPackageToCachedOrigins == null) return false;
Set<Origin> cachedOrigins = sPackageToCachedOrigins.get(new Pair<>(packageName, relation));
if (cachedOrigins == null) return false;
return cachedOrigins.contains(origin);
}
/**
* Callback interface for getting verification results.
*/
public interface OriginVerificationListener {
/**
* To be posted on the handler thread after the verification finishes.
* @param packageName The package name for the origin verification query for this result.
* @param origin The origin that was declared on the query for this result.
* @param verified Whether the given origin was verified to correspond to the given package.
* @param online Whether the device could connect to the internet to perform verification.
* Will be {@code null} if internet was not required for check (eg
* verification had already been attempted this Chrome lifetime and the
* result was cached or the origin was not https).
*/
void onOriginVerified(String packageName, Origin origin, boolean verified, Boolean online);
}
/**
* Main constructor.
* Use {@link OriginVerifier#start(Origin)}
* @param listener The listener who will get the verification result.
* @param packageName The package for the Android application for verification.
* @param relation Digital Asset Links {@link Relation} to use during verification.
*/
public OriginVerifier(
OriginVerificationListener listener, String packageName, @Relation int relation) {
mListener = listener;
mPackageName = packageName;
mSignatureFingerprint = getCertificateSHA256FingerprintForPackage(mPackageName);
mRelation = relation;
}
/**
* Verify the claimed origin for the cached package name asynchronously. This will end up
* making a network request for non-cached origins with a URLFetcher using the last used
* profile as context.
* @param origin The postMessage origin the application is claiming to have. Can't be null.
*/
public void start(@NonNull Origin origin) {
ThreadUtils.assertOnUiThread();
mOrigin = origin;
// Website to app Digital Asset Link verification can be skipped for a specific URL by
// passing a command line flag to ease development.
String disableDalUrl = CommandLine.getInstance().getSwitchValue(
ChromeSwitches.DISABLE_DIGITAL_ASSET_LINK_VERIFICATION);
if (!TextUtils.isEmpty(disableDalUrl)
&& mOrigin.equals(new Origin(disableDalUrl))) {
Log.i(TAG, "Verification skipped for %s due to command line flag.", origin);
ThreadUtils.runOnUiThread(new VerifiedCallback(true, null));
return;
}
String scheme = mOrigin.uri().getScheme();
if (TextUtils.isEmpty(scheme)
|| !UrlConstants.HTTPS_SCHEME.equals(scheme.toLowerCase(Locale.US))) {
Log.i(TAG, "Verification failed for %s as not https.", origin);
BrowserServicesMetrics.recordVerificationResult(
BrowserServicesMetrics.VERIFICATION_RESULT_HTTPS_FAILURE);
ThreadUtils.runOnUiThread(new VerifiedCallback(false, null));
return;
}
// If this origin is cached as verified already, use that.
if (isValidOrigin(mPackageName, origin, mRelation)) {
Log.i(TAG, "Verification succeeded for %s, it was cached.", origin);
BrowserServicesMetrics.recordVerificationResult(
BrowserServicesMetrics.VERIFICATION_RESULT_CACHED_SUCCESS);
ThreadUtils.runOnUiThread(new VerifiedCallback(true, null));
return;
}
if (mNativeOriginVerifier != 0) cleanUp();
if (!BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.isStartupSuccessfullyCompleted()) {
// Early return for testing without native.
return;
}
mNativeOriginVerifier = nativeInit(Profile.getLastUsedProfile().getOriginalProfile());
assert mNativeOriginVerifier != 0;
String relationship = null;
switch (mRelation) {
case CustomTabsService.RELATION_USE_AS_ORIGIN:
relationship = USE_AS_ORIGIN;
break;
case CustomTabsService.RELATION_HANDLE_ALL_URLS:
relationship = HANDLE_ALL_URLS;
break;
default:
assert false;
break;
}
boolean requestSent = nativeVerifyOrigin(mNativeOriginVerifier, mPackageName,
mSignatureFingerprint, mOrigin.toString(), relationship);
if (!requestSent) {
BrowserServicesMetrics.recordVerificationResult(
BrowserServicesMetrics.VERIFICATION_RESULT_REQUEST_FAILURE);
ThreadUtils.runOnUiThread(new VerifiedCallback(false, false));
}
}
/**
* Cleanup native dependencies on this object.
*/
public void cleanUp() {
if (mNativeOriginVerifier == 0) return;
nativeDestroy(mNativeOriginVerifier);
mNativeOriginVerifier = 0;
}
private static PackageInfo getPackageInfo(String packageName) {
PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
// Will return null if there is no package found.
}
return packageInfo;
}
/**
* Computes the SHA256 certificate for the given package name. The app with the given package
* name has to be installed on device. The output will be a 30 long HEX string with : between
* each value.
* @param packageName The package name to query the signature for.
* @return The SHA256 certificate for the package name.
*/
@SuppressLint("PackageManagerGetSignatures")
// https://stackoverflow.com/questions/39192844/android-studio-warning-when-using-packagemanager-get-signatures
static String getCertificateSHA256FingerprintForPackage(String packageName) {
PackageInfo packageInfo = getPackageInfo(packageName);
if (packageInfo == null) return null;
InputStream input = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());
X509Certificate certificate = null;
String hexString = null;
try {
certificate =
(X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(
input);
hexString = byteArrayToHexString(
MessageDigest.getInstance("SHA256").digest(certificate.getEncoded()));
} catch (CertificateEncodingException e) {
Log.w(TAG, "Certificate type X509 encoding failed");
} catch (CertificateException | NoSuchAlgorithmException e) {
// This shouldn't happen.
}
return hexString;
}
/**
* Converts a byte array to hex string with : inserted between each element.
* @param byteArray The array to be converted.
* @return A string with two letters representing each byte and : in between.
*/
static String byteArrayToHexString(byte[] byteArray) {
StringBuilder hexString = new StringBuilder(byteArray.length * 3 - 1);
for (int i = 0; i < byteArray.length; ++i) {
hexString.append(HEX_CHAR_LOOKUP[(byteArray[i] & 0xf0) >>> 4]);
hexString.append(HEX_CHAR_LOOKUP[byteArray[i] & 0xf]);
if (i < (byteArray.length - 1)) hexString.append(':');
}
return hexString.toString();
}
/** Called asynchronously by nativeVerifyOrigin. */
@CalledByNative
private void onOriginVerificationResult(int result) {
switch (result) {
case RelationshipCheckResult.SUCCESS:
BrowserServicesMetrics.recordVerificationResult(
BrowserServicesMetrics.VERIFICATION_RESULT_ONLINE_SUCCESS);
originVerified(true, true);
break;
case RelationshipCheckResult.FAILURE:
BrowserServicesMetrics.recordVerificationResult(
BrowserServicesMetrics.VERIFICATION_RESULT_ONLINE_FAILURE);
originVerified(false, true);
break;
case RelationshipCheckResult.NO_CONNECTION:
Log.i(TAG, "Device is offline, checking saved verification result.");
checkForSavedResult();
break;
default:
assert false;
}
}
/** Deal with the result of an Origin check. Will be called on UI Thread. */
private void originVerified(boolean originVerified, Boolean online) {
Log.i(TAG, "Verification %s.", (originVerified ? "succeeded" : "failed"));
if (originVerified) {
addVerifiedOriginForPackage(mPackageName, mOrigin, mRelation);
}
// We save the result even if there is a failure as a way of overwriting a previously
// successfully verified result that fails on a subsequent check.
saveVerificationResult(originVerified);
if (mListener != null) {
mListener.onOriginVerified(mPackageName, mOrigin, originVerified, online);
}
cleanUp();
}
/**
* Saves the result of a verification to Preferences so we can reuse it when offline.
*/
private void saveVerificationResult(boolean originVerified) {
String link = relationshipToString(mPackageName, mOrigin, mRelation);
Set<String> savedLinks;
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
savedLinks = ChromePreferenceManager.getInstance().getVerifiedDigitalAssetLinks();
}
if (originVerified) {
savedLinks.add(link);
} else {
savedLinks.remove(link);
}
ChromePreferenceManager.getInstance().setVerifiedDigitalAssetLinks(savedLinks);
}
/**
* Checks for a previously saved verification result.
*/
private void checkForSavedResult() {
String link = relationshipToString(mPackageName, mOrigin, mRelation);
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
Set<String> savedLinks =
ChromePreferenceManager.getInstance().getVerifiedDigitalAssetLinks();
boolean verified = savedLinks.contains(link);
BrowserServicesMetrics.recordVerificationResult(verified
? BrowserServicesMetrics.VERIFICATION_RESULT_OFFLINE_SUCCESS
: BrowserServicesMetrics.VERIFICATION_RESULT_OFFLINE_FAILURE);
originVerified(verified, false);
}
}
private static String relationshipToString(String packageName, Origin origin, int relation) {
// Neither package names nor origins contain commas.
return packageName + "," + origin + "," + relation;
}
/**
* Removes any data about sites visited from static variables and Android Preferences.
*/
@CalledByNative
public static void clearBrowsingData() {
ThreadUtils.assertOnUiThread();
if (sPackageToCachedOrigins != null) sPackageToCachedOrigins.clear();
ChromePreferenceManager.getInstance().setVerifiedDigitalAssetLinks(Collections.emptySet());
ChromePreferenceManager.getInstance().clearAllTrustedWebActivityLastDisclosureTimes();
}
private native long nativeInit(Profile profile);
private native boolean nativeVerifyOrigin(long nativeOriginVerifier, String packageName,
String signatureFingerprint, String origin, String relationship);
private native void nativeDestroy(long nativeOriginVerifier);
}