Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4420)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java

Issue 2767333006: Add Digital Asset Links verification for postMessage API (Closed)
Patch Set: destructor Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..34a9a84d20dc2c612ae80b03dbe3d19e13e54d6f
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java
@@ -0,0 +1,227 @@
+// 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.customtabs;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+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.IntentHandler;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.content.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.HashMap;
+import java.util.Map;
+
+/**
+ * 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")
+class OriginVerifier {
+ private static final String TAG = "OriginVerifier";
+ private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();
+ private static Map<String, Uri> sCachedOriginMap;
+ private final OriginVerificationListener mListener;
+ private final String mPackageName;
+ private final String mSignatureFingerprint;
+ private long mNativeOriginVerifier = 0;
+ private Uri mOrigin;
+
+ /**
+ * To be used for prepopulating verified origin for testing functionality.
+ * @param packageName The package name to prepopulate for.
+ * @param origin The origin to add as verified.
+ */
+ @VisibleForTesting
+ static void prePopulateVerifiedOriginForTesting(String packageName, Uri origin) {
+ cacheVerifiedOriginIfNeeded(packageName, origin);
+ }
+
+ private static Uri getPostMessageOriginFromVerifiedOrigin(
+ String packageName, Uri verifiedOrigin) {
+ return Uri.parse(IntentHandler.ANDROID_APP_REFERRER_SCHEME + "://"
+ + verifiedOrigin.getHost() + "/" + packageName);
+ }
+
+ private static void cacheVerifiedOriginIfNeeded(String packageName, Uri origin) {
+ if (sCachedOriginMap == null) sCachedOriginMap = new HashMap<>();
+ if (!sCachedOriginMap.containsKey(packageName)) {
+ sCachedOriginMap.put(packageName, 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.
+ */
+ void onOriginVerified(String packageName, Uri origin, boolean verified);
+ }
+
+ /**
+ * Main constructor.
+ * Use {@link OriginVerifier#start(Uri)}
+ * @param listener The listener who will get the verification result.
+ * @param packageName The package for the Android application for verification.
+ */
+ public OriginVerifier(OriginVerificationListener listener, String packageName) {
+ mListener = listener;
+ mPackageName = packageName;
+ mSignatureFingerprint = getCertificateSHA256FingerprintForPackage(mPackageName);
+ }
+
+ /**
+ * 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 Uri origin) {
+ ThreadUtils.assertOnUiThread();
+ mOrigin = origin;
+
+ // If this origin is cached as verified already, use that.
+ Uri cachedOrigin = getCachedOriginIfExists();
+ if (cachedOrigin != null && cachedOrigin.equals(origin)) {
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ originVerified(true);
+ }
+ });
+ 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;
+ boolean success = nativeVerifyOrigin(
+ mNativeOriginVerifier, mPackageName, mSignatureFingerprint, mOrigin.toString());
+ if (!success) {
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ originVerified(false);
+ }
+ });
+ }
+ }
+
+ /**
+ * Cleanup native dependencies on this object.
+ */
+ 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.
+ */
+ 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();
+ }
+
+ @CalledByNative
+ private void originVerified(boolean originVerified) {
+ if (originVerified) {
+ cacheVerifiedOriginIfNeeded(mPackageName, mOrigin);
+ mOrigin = getPostMessageOriginFromVerifiedOrigin(mPackageName, mOrigin);
+ }
+ mListener.onOriginVerified(mPackageName, mOrigin, originVerified);
+ cleanUp();
+ }
+
+ private Uri getCachedOriginIfExists() {
+ if (sCachedOriginMap == null) return null;
+ return sCachedOriginMap.get(mPackageName);
+ }
+
+ private native long nativeInit(Profile profile);
+ private native boolean nativeVerifyOrigin(long nativeOriginVerifier, String packageName,
+ String signatureFingerprint, String origin);
+ private native void nativeDestroy(long nativeOriginVerifier);
+}

Powered by Google App Engine
This is Rietveld 408576698