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.
5 package org.chromium.chrome.browser;
7 import android.content.Context;
8 import android.os.AsyncTask;
9 import android.util.Log;
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;
21 import javax.crypto.KeyGenerator;
22 import javax.crypto.Mac;
23 import javax.crypto.SecretKey;
24 import javax.crypto.spec.SecretKeySpec;
27 * Authenticate the source of Intents to launch web apps (see e.g. {@link #FullScreenActivity}).
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.
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();
42 private static FutureTask<SecretKey> sMacKeyGenerator;
43 private static SecretKey sKey = null;
48 * @param url The URL to validate.
49 * @param mac The bytes of a previously-calculated MAC.
51 * @return true if the MAC is a valid MAC for the URL, false otherwise.
53 public static boolean isUrlValid(Context context, String url, byte[] mac) {
54 byte[] goodMac = getMacForUrl(context, url);
55 if (goodMac == null) {
58 return constantTimeAreArraysEqual(goodMac, mac);
64 * @param url A URL for which to calculate a MAC.
66 * @return The bytes of a MAC for the URL, or null if a secure MAC was not available.
68 public static byte[] getMacForUrl(Context context, String url) {
69 Mac mac = getMac(context);
73 return mac.doFinal(url.getBytes());
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) {
83 for (int i = 0; i < a.length; i++) {
84 result |= a[i] ^ b[i];
89 private static SecretKey readKeyFromFile(
90 Context context, String basename, String algorithmName) {
91 FileInputStream input = null;
92 File file = context.getFileStreamPath(basename);
94 if (file.length() != MAC_KEY_BYTE_COUNT) {
95 Log.w(TAG, "Could not read key from '" + file + "': invalid file contents");
99 byte[] keyBytes = new byte[MAC_KEY_BYTE_COUNT];
100 input = new FileInputStream(file);
101 if (MAC_KEY_BYTE_COUNT != input.read(keyBytes)) {
106 return new SecretKeySpec(keyBytes, algorithmName);
107 } catch (IllegalArgumentException e) {
110 } catch (Exception e) {
111 Log.w(TAG, "Could not read key from '" + file + "': " + e);
118 } catch (Exception e) {
119 Log.e(TAG, "Could not close key input stream '" + file + "': " + e);
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);
134 FileOutputStream output = new FileOutputStream(file);
135 output.write(keyBytes);
138 } catch (Exception e) {
139 Log.e(TAG, "Could not write key to '" + file + "': " + e);
144 private static SecretKey getKey(Context context) {
145 synchronized (sLock) {
147 SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME);
153 triggerMacKeyGeneration();
155 sKey = sMacKeyGenerator.get();
156 sMacKeyGenerator = null;
157 if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) {
162 } catch (InterruptedException e) {
163 throw new RuntimeException(e);
164 } catch (ExecutionException e) {
165 throw new RuntimeException(e);
173 * Generates the authentication encryption key in a background thread (if necessary).
175 private static void triggerMacKeyGeneration() {
176 synchronized (sLock) {
177 if (sKey != null || sMacKeyGenerator != null) {
181 sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() {
183 public SecretKey call() throws Exception {
184 KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME);
185 SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
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.
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);
199 random.setSeed(seed);
200 generator.init(MAC_KEY_BYTE_COUNT * 8, random);
201 return generator.generateKey();
204 AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator);
208 private static byte[] getRandomBytes(int count) {
209 FileInputStream fis = null;
211 fis = new FileInputStream("/dev/urandom");
212 byte[] bytes = new byte[count];
213 if (bytes.length != fis.read(bytes)) {
217 } catch (Throwable t) {
218 // This causes the ultimate caller, i.e. getMac, to fail.
225 } catch (IOException e) {
226 // Nothing we can do.
232 * @return A Mac, or null if it is not possible to instantiate one.
234 private static Mac getMac(Context context) {
236 SecretKey key = getKey(context);
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.
243 Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME);
246 } catch (GeneralSecurityException e) {
247 Log.w(TAG, "Error in creating MAC instance", e);