summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorpalmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-25 19:43:10 +0000
committerpalmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-25 19:43:10 +0000
commit5d4c422a0fbbc4393b4e816d4f0f3b6472cd0377 (patch)
treee675d877a6254cf0fcd8577012da226dcbe77abc
parentd96104b2030a31a9948cc29b218dd1222279ab60 (diff)
downloadchromium_src-5d4c422a0fbbc4393b4e816d4f0f3b6472cd0377.zip
chromium_src-5d4c422a0fbbc4393b4e816d4f0f3b6472cd0377.tar.gz
chromium_src-5d4c422a0fbbc4393b4e816d4f0f3b6472cd0377.tar.bz2
Move WebappAuthenticator to its new package.
R=dtrainor@chromium.org TBR=dfalcantara@chromium.org, klobag@chromium.org BUG=285924 Review URL: https://codereview.chromium.org/23710085 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@225230 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/WebappAuthenticator.java251
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/WebappAuthenticatorTest.java23
2 files changed, 274 insertions, 0 deletions
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/WebappAuthenticator.java b/chrome/android/java/src/org/chromium/chrome/browser/WebappAuthenticator.java
new file mode 100644
index 0000000..8a3f4f2
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/WebappAuthenticator.java
@@ -0,0 +1,251 @@
+// Copyright 2013 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;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Authenticate the source of Intents to launch web apps (see e.g. {@link #FullScreenActivity}).
+ *
+ * Chrome does not keep a store of valid URLs for installed web apps (because it cannot know when
+ * any have been uninstalled). Therefore, upon installation, it tells the Launcher a message
+ * authentication code (MAC) along with the URL for the web app, and then Chrome can verify the MAC
+ * when starting e.g. {@link #FullScreenActivity}. Chrome can thus distinguish between legitimate,
+ * installed web apps and arbitrary other URLs.
+ */
+public class WebappAuthenticator {
+ private static final String TAG = "WebappAuthenticator";
+ private static final String MAC_ALGORITHM_NAME = "HmacSHA256";
+ private static final String MAC_KEY_BASENAME = "webapp-authenticator";
+ private static final int MAC_KEY_BYTE_COUNT = 32;
+ private static final Object sLock = new Object();
+
+ private static FutureTask<SecretKey> sMacKeyGenerator;
+ private static SecretKey sKey = null;
+
+ /**
+ * @see #getMacForUrl
+ *
+ * @param url The URL to validate.
+ * @param mac The bytes of a previously-calculated MAC.
+ *
+ * @return true if the MAC is a valid MAC for the URL, false otherwise.
+ */
+ public static boolean isUrlValid(Context context, String url, byte[] mac) {
+ byte[] goodMac = getMacForUrl(context, url);
+ if (goodMac == null) {
+ return false;
+ }
+ return constantTimeAreArraysEqual(goodMac, mac);
+ }
+
+ /**
+ * @see #isUrlValid
+ *
+ * @param url A URL for which to calculate a MAC.
+ *
+ * @return The bytes of a MAC for the URL, or null if a secure MAC was not available.
+ */
+ public static byte[] getMacForUrl(Context context, String url) {
+ Mac mac = getMac(context);
+ if (mac == null) {
+ return null;
+ }
+ return mac.doFinal(url.getBytes());
+ }
+
+ // TODO(palmer): Put this method, and as much of this class as possible, in a utility class.
+ private static boolean constantTimeAreArraysEqual(byte[] a, byte[] b) {
+ if (a.length != b.length) {
+ return false;
+ }
+
+ int result = 0;
+ for (int i = 0; i < a.length; i++) {
+ result |= a[i] ^ b[i];
+ }
+ return result == 0;
+ }
+
+ private static SecretKey readKeyFromFile(
+ Context context, String basename, String algorithmName) {
+ FileInputStream input = null;
+ File file = context.getFileStreamPath(basename);
+ try {
+ if (file.length() != MAC_KEY_BYTE_COUNT) {
+ Log.w(TAG, "Could not read key from '" + file + "': invalid file contents");
+ return null;
+ }
+
+ byte[] keyBytes = new byte[MAC_KEY_BYTE_COUNT];
+ input = new FileInputStream(file);
+ if (MAC_KEY_BYTE_COUNT != input.read(keyBytes)) {
+ return null;
+ }
+
+ try {
+ return new SecretKeySpec(keyBytes, algorithmName);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Could not read key from '" + file + "': " + e);
+ return null;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Could not close key input stream '" + file + "': " + e);
+ }
+ }
+ }
+
+ private static boolean writeKeyToFile(Context context, String basename, SecretKey key) {
+ File file = context.getFileStreamPath(basename);
+ byte[] keyBytes = key.getEncoded();
+ if (MAC_KEY_BYTE_COUNT != keyBytes.length) {
+ Log.e(TAG, "writeKeyToFile got key encoded bytes length " + keyBytes.length +
+ "; expected " + MAC_KEY_BYTE_COUNT);
+ return false;
+ }
+
+ try {
+ FileOutputStream output = new FileOutputStream(file);
+ output.write(keyBytes);
+ output.close();
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Could not write key to '" + file + "': " + e);
+ return false;
+ }
+ }
+
+ private static SecretKey getKey(Context context) {
+ synchronized (sLock) {
+ if (sKey == null) {
+ SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME);
+ if (key != null) {
+ sKey = key;
+ return sKey;
+ }
+
+ triggerMacKeyGeneration();
+ try {
+ sKey = sMacKeyGenerator.get();
+ sMacKeyGenerator = null;
+ if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) {
+ sKey = null;
+ return null;
+ }
+ return sKey;
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return sKey;
+ }
+ }
+
+ /**
+ * Generates the authentication encryption key in a background thread (if necessary).
+ */
+ private static void triggerMacKeyGeneration() {
+ synchronized (sLock) {
+ if (sKey != null || sMacKeyGenerator != null) {
+ return;
+ }
+
+ sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() {
+ @Override
+ public SecretKey call() throws Exception {
+ KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME);
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+
+ // Versions of SecureRandom from Android <= 4.3 do not seed themselves as
+ // securely as possible. This workaround should suffice until the fixed version
+ // is deployed to all users. getRandomBytes, which reads from /dev/urandom,
+ // which is as good as the platform can get.
+ //
+ // TODO(palmer): Consider getting rid of this once the updated platform has
+ // shipped to everyone. Alternately, leave this in as a defense against other
+ // bugs in SecureRandom.
+ byte[] seed = getRandomBytes(MAC_KEY_BYTE_COUNT);
+ if (seed == null) {
+ return null;
+ }
+ random.setSeed(seed);
+ generator.init(MAC_KEY_BYTE_COUNT * 8, random);
+ return generator.generateKey();
+ }
+ });
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator);
+ }
+ }
+
+ private static byte[] getRandomBytes(int count) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream("/dev/urandom");
+ byte[] bytes = new byte[count];
+ if (bytes.length != fis.read(bytes)) {
+ return null;
+ }
+ return bytes;
+ } catch (Throwable t) {
+ // This causes the ultimate caller, i.e. getMac, to fail.
+ return null;
+ } finally {
+ try {
+ if (fis != null) {
+ fis.close();
+ }
+ } catch (IOException e) {
+ // Nothing we can do.
+ }
+ }
+ }
+
+ /**
+ * @return A Mac, or null if it is not possible to instantiate one.
+ */
+ private static Mac getMac(Context context) {
+ try {
+ SecretKey key = getKey(context);
+ if (key == null) {
+ // getKey should have invoked triggerMacKeyGeneration, which should have set the
+ // random seed and generated a key from it. If not, there is a problem with the
+ // random number generator, and we must not claim that authentication can work.
+ return null;
+ }
+ Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME);
+ mac.init(key);
+ return mac;
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Error in creating MAC instance", e);
+ return null;
+ }
+ }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/WebappAuthenticatorTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/WebappAuthenticatorTest.java
new file mode 100644
index 0000000..2d075f1
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/WebappAuthenticatorTest.java
@@ -0,0 +1,23 @@
+// Copyright 2013 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;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+public class WebappAuthenticatorTest extends InstrumentationTestCase {
+ @SmallTest
+ public void testAuthentication() {
+ Context context = getInstrumentation().getTargetContext();
+ String url = "http://www.example.org/hello.html";
+ byte[] mac = WebappAuthenticator.getMacForUrl(context, url);
+ assertNotNull(mac);
+ assertTrue(WebappAuthenticator.isUrlValid(context, url, mac));
+ assertFalse(WebappAuthenticator.isUrlValid(context, url + "?goats=true", mac));
+ mac[4] += 1;
+ assertFalse(WebappAuthenticator.isUrlValid(context, url, mac));
+ }
+}