// 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; } } }