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); |
+} |