diff options
author | palmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-25 19:43:10 +0000 |
---|---|---|
committer | palmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-25 19:43:10 +0000 |
commit | 5d4c422a0fbbc4393b4e816d4f0f3b6472cd0377 (patch) | |
tree | e675d877a6254cf0fcd8577012da226dcbe77abc | |
parent | d96104b2030a31a9948cc29b218dd1222279ab60 (diff) | |
download | chromium_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.java | 251 | ||||
-rw-r--r-- | chrome/android/javatests/src/org/chromium/chrome/browser/WebappAuthenticatorTest.java | 23 |
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)); + } +} |