1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser; 6 7 import android.content.Context; 8 import android.os.AsyncTask; 9 import android.util.Log; 10 11 import java.io.File; 12 import java.io.FileInputStream; 13 import java.io.FileOutputStream; 14 import java.io.IOException; 15 import java.security.GeneralSecurityException; 16 import java.security.SecureRandom; 17 import java.util.concurrent.Callable; 18 import java.util.concurrent.ExecutionException; 19 import java.util.concurrent.FutureTask; 20 21 import javax.crypto.KeyGenerator; 22 import javax.crypto.Mac; 23 import javax.crypto.SecretKey; 24 import javax.crypto.spec.SecretKeySpec; 25 26 /** 27 * Authenticate the source of Intents to launch web apps (see e.g. {@link #FullScreenActivity}). 28 * 29 * Chrome does not keep a store of valid URLs for installed web apps (because it cannot know when 30 * any have been uninstalled). Therefore, upon installation, it tells the Launcher a message 31 * authentication code (MAC) along with the URL for the web app, and then Chrome can verify the MAC 32 * when starting e.g. {@link #FullScreenActivity}. Chrome can thus distinguish between legitimate, 33 * installed web apps and arbitrary other URLs. 34 */ 35 public class WebappAuthenticator { 36 private static final String TAG = "WebappAuthenticator"; 37 private static final String MAC_ALGORITHM_NAME = "HmacSHA256"; 38 private static final String MAC_KEY_BASENAME = "webapp-authenticator"; 39 private static final int MAC_KEY_BYTE_COUNT = 32; 40 private static final Object sLock = new Object(); 41 42 private static FutureTask<SecretKey> sMacKeyGenerator; 43 private static SecretKey sKey = null; 44 45 /** 46 * @see #getMacForUrl 47 * 48 * @param url The URL to validate. 49 * @param mac The bytes of a previously-calculated MAC. 50 * 51 * @return true if the MAC is a valid MAC for the URL, false otherwise. 52 */ isUrlValid(Context context, String url, byte[] mac)53 public static boolean isUrlValid(Context context, String url, byte[] mac) { 54 byte[] goodMac = getMacForUrl(context, url); 55 if (goodMac == null) { 56 return false; 57 } 58 return constantTimeAreArraysEqual(goodMac, mac); 59 } 60 61 /** 62 * @see #isUrlValid 63 * 64 * @param url A URL for which to calculate a MAC. 65 * 66 * @return The bytes of a MAC for the URL, or null if a secure MAC was not available. 67 */ getMacForUrl(Context context, String url)68 public static byte[] getMacForUrl(Context context, String url) { 69 Mac mac = getMac(context); 70 if (mac == null) { 71 return null; 72 } 73 return mac.doFinal(url.getBytes()); 74 } 75 76 // TODO(palmer): Put this method, and as much of this class as possible, in a utility class. constantTimeAreArraysEqual(byte[] a, byte[] b)77 private static boolean constantTimeAreArraysEqual(byte[] a, byte[] b) { 78 if (a.length != b.length) { 79 return false; 80 } 81 82 int result = 0; 83 for (int i = 0; i < a.length; i++) { 84 result |= a[i] ^ b[i]; 85 } 86 return result == 0; 87 } 88 readKeyFromFile( Context context, String basename, String algorithmName)89 private static SecretKey readKeyFromFile( 90 Context context, String basename, String algorithmName) { 91 FileInputStream input = null; 92 File file = context.getFileStreamPath(basename); 93 try { 94 if (file.length() != MAC_KEY_BYTE_COUNT) { 95 Log.w(TAG, "Could not read key from '" + file + "': invalid file contents"); 96 return null; 97 } 98 99 byte[] keyBytes = new byte[MAC_KEY_BYTE_COUNT]; 100 input = new FileInputStream(file); 101 if (MAC_KEY_BYTE_COUNT != input.read(keyBytes)) { 102 return null; 103 } 104 105 try { 106 return new SecretKeySpec(keyBytes, algorithmName); 107 } catch (IllegalArgumentException e) { 108 return null; 109 } 110 } catch (Exception e) { 111 Log.w(TAG, "Could not read key from '" + file + "': " + e); 112 return null; 113 } finally { 114 try { 115 if (input != null) { 116 input.close(); 117 } 118 } catch (Exception e) { 119 Log.e(TAG, "Could not close key input stream '" + file + "': " + e); 120 } 121 } 122 } 123 writeKeyToFile(Context context, String basename, SecretKey key)124 private static boolean writeKeyToFile(Context context, String basename, SecretKey key) { 125 File file = context.getFileStreamPath(basename); 126 byte[] keyBytes = key.getEncoded(); 127 if (MAC_KEY_BYTE_COUNT != keyBytes.length) { 128 Log.e(TAG, "writeKeyToFile got key encoded bytes length " + keyBytes.length + 129 "; expected " + MAC_KEY_BYTE_COUNT); 130 return false; 131 } 132 133 try { 134 FileOutputStream output = new FileOutputStream(file); 135 output.write(keyBytes); 136 output.close(); 137 return true; 138 } catch (Exception e) { 139 Log.e(TAG, "Could not write key to '" + file + "': " + e); 140 return false; 141 } 142 } 143 getKey(Context context)144 private static SecretKey getKey(Context context) { 145 synchronized (sLock) { 146 if (sKey == null) { 147 SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME); 148 if (key != null) { 149 sKey = key; 150 return sKey; 151 } 152 153 triggerMacKeyGeneration(); 154 try { 155 sKey = sMacKeyGenerator.get(); 156 sMacKeyGenerator = null; 157 if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) { 158 sKey = null; 159 return null; 160 } 161 return sKey; 162 } catch (InterruptedException e) { 163 throw new RuntimeException(e); 164 } catch (ExecutionException e) { 165 throw new RuntimeException(e); 166 } 167 } 168 return sKey; 169 } 170 } 171 172 /** 173 * Generates the authentication encryption key in a background thread (if necessary). 174 */ triggerMacKeyGeneration()175 private static void triggerMacKeyGeneration() { 176 synchronized (sLock) { 177 if (sKey != null || sMacKeyGenerator != null) { 178 return; 179 } 180 181 sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() { 182 @Override 183 public SecretKey call() throws Exception { 184 KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME); 185 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 186 187 // Versions of SecureRandom from Android <= 4.3 do not seed themselves as 188 // securely as possible. This workaround should suffice until the fixed version 189 // is deployed to all users. getRandomBytes, which reads from /dev/urandom, 190 // which is as good as the platform can get. 191 // 192 // TODO(palmer): Consider getting rid of this once the updated platform has 193 // shipped to everyone. Alternately, leave this in as a defense against other 194 // bugs in SecureRandom. 195 byte[] seed = getRandomBytes(MAC_KEY_BYTE_COUNT); 196 if (seed == null) { 197 return null; 198 } 199 random.setSeed(seed); 200 generator.init(MAC_KEY_BYTE_COUNT * 8, random); 201 return generator.generateKey(); 202 } 203 }); 204 AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator); 205 } 206 } 207 getRandomBytes(int count)208 private static byte[] getRandomBytes(int count) { 209 FileInputStream fis = null; 210 try { 211 fis = new FileInputStream("/dev/urandom"); 212 byte[] bytes = new byte[count]; 213 if (bytes.length != fis.read(bytes)) { 214 return null; 215 } 216 return bytes; 217 } catch (Throwable t) { 218 // This causes the ultimate caller, i.e. getMac, to fail. 219 return null; 220 } finally { 221 try { 222 if (fis != null) { 223 fis.close(); 224 } 225 } catch (IOException e) { 226 // Nothing we can do. 227 } 228 } 229 } 230 231 /** 232 * @return A Mac, or null if it is not possible to instantiate one. 233 */ getMac(Context context)234 private static Mac getMac(Context context) { 235 try { 236 SecretKey key = getKey(context); 237 if (key == null) { 238 // getKey should have invoked triggerMacKeyGeneration, which should have set the 239 // random seed and generated a key from it. If not, there is a problem with the 240 // random number generator, and we must not claim that authentication can work. 241 return null; 242 } 243 Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME); 244 mac.init(key); 245 return mac; 246 } catch (GeneralSecurityException e) { 247 Log.w(TAG, "Error in creating MAC instance", e); 248 return null; 249 } 250 } 251 } 252