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 5package org.chromium.chrome.browser; 6 7import android.content.Context; 8import android.os.AsyncTask; 9import android.util.Log; 10 11import java.io.File; 12import java.io.FileInputStream; 13import java.io.FileOutputStream; 14import java.io.IOException; 15import java.security.GeneralSecurityException; 16import java.security.SecureRandom; 17import java.util.concurrent.Callable; 18import java.util.concurrent.ExecutionException; 19import java.util.concurrent.FutureTask; 20 21import javax.crypto.KeyGenerator; 22import javax.crypto.Mac; 23import javax.crypto.SecretKey; 24import 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 */ 35public 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 */ 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 */ 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. 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 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 (IOException e) { 119 Log.e(TAG, "Could not close key input stream '" + file + "': " + e); 120 } 121 } 122 } 123 124 private static boolean writeKeyToFile(Context context, String basename, SecretKey key) { 125 File file = context.getFileStreamPath(basename); 126 byte[] keyBytes = key.getEncoded(); 127 FileOutputStream output = null; 128 if (MAC_KEY_BYTE_COUNT != keyBytes.length) { 129 Log.e(TAG, "writeKeyToFile got key encoded bytes length " + keyBytes.length + 130 "; expected " + MAC_KEY_BYTE_COUNT); 131 return false; 132 } 133 134 try { 135 output = new FileOutputStream(file); 136 output.write(keyBytes); 137 return true; 138 } catch (Exception e) { 139 Log.e(TAG, "Could not write key to '" + file + "': " + e); 140 return false; 141 } finally { 142 try { 143 if (output != null) { 144 output.close(); 145 } 146 } catch (IOException e) { 147 Log.e(TAG, "Could not close key output stream '" + file + "': " + e); 148 } 149 } 150 } 151 152 private static SecretKey getKey(Context context) { 153 synchronized (sLock) { 154 if (sKey == null) { 155 SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME); 156 if (key != null) { 157 sKey = key; 158 return sKey; 159 } 160 161 triggerMacKeyGeneration(); 162 try { 163 sKey = sMacKeyGenerator.get(); 164 sMacKeyGenerator = null; 165 if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) { 166 sKey = null; 167 return null; 168 } 169 return sKey; 170 } catch (InterruptedException e) { 171 throw new RuntimeException(e); 172 } catch (ExecutionException e) { 173 throw new RuntimeException(e); 174 } 175 } 176 return sKey; 177 } 178 } 179 180 /** 181 * Generates the authentication encryption key in a background thread (if necessary). 182 */ 183 private static void triggerMacKeyGeneration() { 184 synchronized (sLock) { 185 if (sKey != null || sMacKeyGenerator != null) { 186 return; 187 } 188 189 sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() { 190 @Override 191 public SecretKey call() throws Exception { 192 KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME); 193 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 194 195 // Versions of SecureRandom from Android <= 4.3 do not seed themselves as 196 // securely as possible. This workaround should suffice until the fixed version 197 // is deployed to all users. getRandomBytes, which reads from /dev/urandom, 198 // which is as good as the platform can get. 199 // 200 // TODO(palmer): Consider getting rid of this once the updated platform has 201 // shipped to everyone. Alternately, leave this in as a defense against other 202 // bugs in SecureRandom. 203 byte[] seed = getRandomBytes(MAC_KEY_BYTE_COUNT); 204 if (seed == null) { 205 return null; 206 } 207 random.setSeed(seed); 208 generator.init(MAC_KEY_BYTE_COUNT * 8, random); 209 return generator.generateKey(); 210 } 211 }); 212 AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator); 213 } 214 } 215 216 private static byte[] getRandomBytes(int count) { 217 FileInputStream fis = null; 218 try { 219 fis = new FileInputStream("/dev/urandom"); 220 byte[] bytes = new byte[count]; 221 if (bytes.length != fis.read(bytes)) { 222 return null; 223 } 224 return bytes; 225 } catch (Throwable t) { 226 // This causes the ultimate caller, i.e. getMac, to fail. 227 return null; 228 } finally { 229 try { 230 if (fis != null) { 231 fis.close(); 232 } 233 } catch (IOException e) { 234 // Nothing we can do. 235 } 236 } 237 } 238 239 /** 240 * @return A Mac, or null if it is not possible to instantiate one. 241 */ 242 private static Mac getMac(Context context) { 243 try { 244 SecretKey key = getKey(context); 245 if (key == null) { 246 // getKey should have invoked triggerMacKeyGeneration, which should have set the 247 // random seed and generated a key from it. If not, there is a problem with the 248 // random number generator, and we must not claim that authentication can work. 249 return null; 250 } 251 Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME); 252 mac.init(key); 253 return mac; 254 } catch (GeneralSecurityException e) { 255 Log.w(TAG, "Error in creating MAC instance", e); 256 return null; 257 } 258 } 259} 260