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..bb06112596396d834c2eb19920528ab1dc714351 |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java |
@@ -0,0 +1,167 @@ |
+// 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.VisibleForTesting; |
+import org.chromium.base.annotations.CalledByNative; |
+import org.chromium.base.annotations.JNINamespace; |
+import org.chromium.chrome.browser.IntentHandler; |
+ |
+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 |
Benoit L
2017/04/07 15:04:54
nit: Add a comment about caching?
Yusuf
2017/04/07 23:38:41
Done.
|
+ * a postMessage origin. |
+ */ |
+@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 long mNativeOriginVerifier = 0; |
+ private final OriginVerificationListener mListener; |
+ private final String mPackageName; |
+ private final String mSignatureFingerprint; |
+ 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); |
+ } |
+ } |
+ |
+ private Uri getCachedOriginIfExists() { |
+ if (sCachedOriginMap == null) return null; |
+ return sCachedOriginMap.get(mPackageName); |
+ } |
+ |
+ public interface OriginVerificationListener { |
+ void onOriginVerified(String packageName, Uri origin, boolean result); |
+ } |
+ |
+ /** |
+ * 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. |
+ * @param origin The postMessage origin the application is claiming to have. Can't be null |
+ */ |
+ public void start(@NonNull Uri origin) { |
+ mOrigin = origin; |
+ |
+ // If this origin is cached as verified already, use that. |
+ Uri cachedOrigin = getCachedOriginIfExists(); |
+ if (cachedOrigin != null && cachedOrigin.equals(origin)) { |
+ originVerified(true); |
+ return; |
+ } |
+ |
+ if (mNativeOriginVerifier != 0) cleanUp(); |
+ mNativeOriginVerifier = nativeInit(); |
+ if (mNativeOriginVerifier == 0) return; |
+ nativeVerifyOrigin( |
+ mNativeOriginVerifier, mPackageName, mSignatureFingerprint, mOrigin.toString()); |
+ } |
+ |
+ private void cleanUp() { |
+ nativeDestroy(mNativeOriginVerifier); |
+ mNativeOriginVerifier = 0; |
+ } |
+ |
+ private String getCertificateSHA256FingerprintForPackage(String packageName) { |
+ PackageManager pm = ContextUtils.getApplicationContext().getPackageManager(); |
+ PackageInfo packageInfo = null; |
+ try { |
+ packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); |
+ } catch (PackageManager.NameNotFoundException e) { |
+ 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 (CertificateException | NoSuchAlgorithmException e) { |
+ if (e instanceof CertificateEncodingException) { |
Benoit L
2017/04/07 15:04:54
You can put multiple catch blocks, provided that t
Yusuf
2017/04/07 23:38:41
Done.
|
+ Log.w(TAG, "Certificate type X509 encoding failed"); |
+ } |
+ return null; |
+ } |
+ return hexString; |
+ } |
+ |
+ private static String byteArrayToHexString(byte[] byteArray) { |
+ StringBuilder hexString = new StringBuilder(byteArray.length * 2); |
Benoit L
2017/04/07 15:04:54
This length computation doesn't account for the de
Yusuf
2017/04/07 23:38:41
Done.
|
+ 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 native long nativeInit(); |
+ private native void nativeVerifyOrigin(long nativeOriginVerifier, String packageName, |
+ String signatureFingerprint, String origin); |
+ private native void nativeDestroy(long nativeOriginVerifier); |
+} |